├── .gitattributes ├── docs ├── flow.png └── ImageSource.pptx ├── tests ├── AnalyzerSetupVerification │ ├── TestProject │ │ ├── Usings.cs │ │ ├── UnitTest1.cs │ │ └── TestProject.csproj │ ├── Project │ │ ├── Class1.cs │ │ └── Project.csproj │ ├── AnotherProject │ │ ├── Class1.cs │ │ └── AnotherProject.csproj │ ├── OtherProject │ │ ├── Class1.cs │ │ └── OtherProject.csproj │ ├── SolutionWithoutTests.sln │ └── SolutionWithTests.sln ├── LivingDocumentation.Descriptions.Tests │ ├── MethodDescriptionTests.cs │ ├── ArgumentDescriptionTests.cs │ ├── EnumMemberDescriptionTests.cs │ ├── AttributeDescriptionTests.cs │ ├── ConstructureDescriptionTests.cs │ ├── InvocationDescriptionTests.cs │ ├── ParameterDescriptionTests.cs │ ├── LivingDocumentation.Descriptions.Tests.csproj │ ├── AssignmentDescriptionTests.cs │ ├── AttributeArgumentDescriptionTests.cs │ ├── EventDescriptionTests.cs │ ├── FieldDescriptionTests.cs │ ├── DocumentationComments │ │ └── DocumentationCommentsDescriptionTests.cs │ ├── PropertyDescriptionTests.cs │ └── MemberDescriptionTests.cs ├── LivingDocumentation.Analyzer.Tests │ ├── PartialClassTests.cs │ ├── LivingDocumentation.Analyzer.Tests.csproj │ ├── EnumModifierTests.cs │ ├── ForEachTests.cs │ ├── TestHelper.cs │ ├── Analyzers │ │ └── BranchingAnalyzerTests.cs │ ├── StructModifierTests.cs │ ├── AnalyzerSetup │ │ └── AnalyzerSetupTests.cs │ ├── MethodModifierTests.cs │ ├── FieldDeclarationTests.cs │ ├── ClassModifierTests.cs │ └── EventDeclarationTests.cs ├── LivingDocumentation.Serializations.Tests │ ├── LivingDocumentation.Serializations.Tests.csproj │ ├── TestHelper.cs │ └── SerializationTests.cs ├── LivingDocumentation.Extensions.Tests │ ├── LivingDocumentation.Extensions.Tests.csproj │ ├── StringExtensionsTests.cs │ └── IHaveModifiersExtensionsTests.cs └── LivingDocumentation.RenderExtensions.Tests │ ├── LivingDocumentation.RenderExtensions.Tests.csproj │ ├── IEnumerableMethodDescriptionExtensionsTests.cs │ ├── IEnumerableStringExtensionsTests.cs │ ├── InvocationDescriptionExtensionsTests.MatchesMethod.cs │ ├── TypeDescriptionListExtensionsTests.PopulateInheritedMembers.cs │ ├── IEnumerableIAttributeDescriptionExtensionsTests.cs │ ├── InvocationDescriptionExtensionsTests.MatchesParameters.cs │ └── TypeDescriptionListExtensionsTests.PopulateInheritedBaseTypes.cs ├── src ├── LivingDocumentation.Statements │ ├── ForEach.cs │ ├── IfElseSection.cs │ ├── SwitchSection.cs │ ├── If.cs │ ├── Switch.cs │ └── LivingDocumentation.Statements.csproj ├── LivingDocumentation.Abstractions │ ├── IHaveModifiers.cs │ ├── TypeType.cs │ ├── Descriptions │ │ ├── IAttributeArgumentDescription.cs │ │ ├── IAttributeDescription.cs │ │ └── IParameterDescription.cs │ ├── MemberType.cs │ ├── IMemberable.cs │ ├── IHaveMethodBody.cs │ ├── Modifier.cs │ ├── Statement.cs │ ├── IHaveDocumentationComments.cs │ └── LivingDocumentation.Abstractions.csproj ├── LivingDocumentation.UML │ ├── Fragments │ │ ├── Interactions.cs │ │ ├── AltSection.cs │ │ ├── Alt.cs │ │ ├── Arrow.cs │ │ ├── InteractionFragment.cs │ │ └── InteractionFragmentExtensions.cs │ ├── IHaveModifiersExtensions.cs │ └── LivingDocumentation.UML.csproj ├── LivingDocumentation.Descriptions │ ├── ReturnDescription.cs │ ├── DocumentationComments │ │ ├── Block.cs │ │ ├── Attribute.cs │ │ ├── Inline.cs │ │ ├── List.cs │ │ ├── ListType.cs │ │ └── Section.cs │ ├── EnumMemberDescription.cs │ ├── ArgumentDescription.cs │ ├── EventDescription.cs │ ├── FieldDescription.cs │ ├── InvocationDescription.cs │ ├── AssignmentDescription.cs │ ├── AttributeArgumentDescription.cs │ ├── PropertyDescription.cs │ ├── AttributeDescription.cs │ ├── ParameterDescription.cs │ ├── Json │ │ └── ConcreteTypeConverter.cs │ ├── ConstructorDescription.cs │ ├── MethodDescription.cs │ ├── MemberDescription.cs │ ├── LivingDocumentation.Descriptions.csproj │ └── TypeDescription.cs ├── LivingDocumentation.Render │ ├── IEnumerableMethodDescriptionExtensions.cs │ ├── IEnumerableStringExtensions.cs │ ├── IEnumerableIAttributeDescriptionExtensions.cs │ ├── InvocationDescriptionExtensions.cs │ ├── LivingDocumentation.RenderExtensions.csproj │ └── StringExtensions.cs ├── LivingDocumentation.Analyzer │ ├── Analyzers │ │ ├── LoopingAnalyzer.cs │ │ └── BranchingAnalyzer.cs │ ├── Extensions │ │ ├── SemanticModelExtensions.cs │ │ └── ExpressionSyntaxExtensions.cs │ ├── Options.cs │ ├── AnalyzerSetup.cs │ ├── LivingDocumentation.Analyzer.csproj │ └── Program.cs ├── LivingDocumentation.Json │ ├── JsonDefaults.cs │ ├── SkipEmptyCollectionsContractResolver.cs │ └── LivingDocumentation.Json.csproj └── LivingDocumentation.Extensions │ ├── StringExtensions.cs │ ├── LivingDocumentation.Extensions.csproj │ └── IHaveModifiersExtensions.cs ├── GitVersion.yml ├── samples └── LivingDocumentation.eShopOnContainers │ ├── LivingDocumentation.Sample.eShopOnContainers.csproj │ ├── Program.cs │ ├── AggregateRenderer.cs │ └── eShopOnContainerExtensions.cs ├── LivingDocumentation.runsettings ├── LICENSE ├── .github └── workflows │ └── ci.yaml ├── readme.md └── .gitignore /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /docs/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eNeRGy164/LivingDocumentation/HEAD/docs/flow.png -------------------------------------------------------------------------------- /docs/ImageSource.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eNeRGy164/LivingDocumentation/HEAD/docs/ImageSource.pptx -------------------------------------------------------------------------------- /tests/AnalyzerSetupVerification/TestProject/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.VisualStudio.TestTools.UnitTesting; -------------------------------------------------------------------------------- /tests/AnalyzerSetupVerification/Project/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Project 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/AnalyzerSetupVerification/AnotherProject/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace AnotherProject 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } -------------------------------------------------------------------------------- /tests/AnalyzerSetupVerification/OtherProject/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace OtherProject 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Statements/ForEach.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("ForEach")] 4 | public class ForEach : Statement 5 | { 6 | public string? Expression { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Abstractions/IHaveModifiers.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public interface IHaveModifiers 4 | { 5 | [JsonProperty(Order = -2)] 6 | Modifier Modifiers { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Abstractions/TypeType.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public enum TypeType 4 | { 5 | Class = 0, 6 | Interface = 1, 7 | Struct = 2, 8 | Enum = 3 9 | } 10 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Statements/IfElseSection.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("IfElse {Condition}")] 4 | public class IfElseSection : Statement 5 | { 6 | public string? Condition { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Statements/SwitchSection.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("Switch Section {Labels}")] 4 | public class SwitchSection : Statement 5 | { 6 | public List Labels { get; } = []; 7 | } 8 | -------------------------------------------------------------------------------- /src/LivingDocumentation.UML/Fragments/Interactions.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Uml; 2 | 3 | /// 4 | /// Represents a list of fragments on the same level. 5 | /// 6 | public class Interactions : InteractionFragment 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /tests/AnalyzerSetupVerification/TestProject/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | namespace TestProject 2 | { 3 | [TestClass] 4 | public class UnitTest1 5 | { 6 | [TestMethod] 7 | public void TestMethod1() 8 | { 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | mode: Mainline 2 | branches: 3 | feature: 4 | regex: ^(?!master|main|pull) 5 | tag: beta 6 | master: 7 | regex: ^main$ 8 | pull-request: 9 | tag: rc 10 | tag-number-pattern: '' 11 | ignore: 12 | sha: [] 13 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/ReturnDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("Return {Expression}")] 4 | public class ReturnDescription(string expression) : Statement 5 | { 6 | public string Expression { get; } = expression; 7 | } 8 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Abstractions/Descriptions/IAttributeArgumentDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public interface IAttributeArgumentDescription 4 | { 5 | string Type { get; } 6 | 7 | string Name { get; } 8 | 9 | string Value { get; } 10 | } 11 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Abstractions/MemberType.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public enum MemberType 4 | { 5 | Field = 0, 6 | Method = 1, 7 | Property = 2, 8 | Constructor = 3, 9 | EnumMember = 4, 10 | Event = 5 11 | } 12 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/DocumentationComments/Block.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.DocumentationComments; 2 | 3 | public class Block 4 | { 5 | public const string Code = "code"; 6 | public const string List = "list"; 7 | public const string Para = "para"; 8 | } 9 | -------------------------------------------------------------------------------- /tests/AnalyzerSetupVerification/Project/Project.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Abstractions/Descriptions/IAttributeDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public interface IAttributeDescription 4 | { 5 | string Type { get; } 6 | 7 | string Name { get; } 8 | 9 | List Arguments { get; } 10 | } 11 | -------------------------------------------------------------------------------- /tests/AnalyzerSetupVerification/OtherProject/OtherProject.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/AnalyzerSetupVerification/AnotherProject/AnotherProject.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Abstractions/Descriptions/IParameterDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public interface IParameterDescription 4 | { 5 | string Type { get; } 6 | 7 | string Name { get; } 8 | 9 | bool HasDefaultValue { get; } 10 | 11 | List Attributes { get; } 12 | } 13 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/DocumentationComments/Attribute.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.DocumentationComments; 2 | 3 | public class Attribute 4 | { 5 | public const string Name = "name"; 6 | public const string CRef = "cref"; 7 | public const string Type = "type"; 8 | public const string Start = "start"; 9 | } 10 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/DocumentationComments/Inline.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.DocumentationComments; 2 | 3 | public class Inline 4 | { 5 | public const string C = "c"; 6 | public const string ParamRef = "paramref"; 7 | public const string TypeParamRef = "typeparamref"; 8 | public const string See = "see"; 9 | } 10 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/DocumentationComments/List.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.DocumentationComments; 2 | 3 | public class List 4 | { 5 | public const string Item = "item"; 6 | public const string Term = "term"; 7 | public const string Description = "description"; 8 | public const string ListHeader = "listHeader"; 9 | } 10 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/DocumentationComments/ListType.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.DocumentationComments; 2 | 3 | public class ListType 4 | { 5 | public const string Bullet = "bullet"; 6 | public const string Number = "number"; 7 | public const string Definition = "definition"; 8 | public const string Table = "table"; 9 | } 10 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Abstractions/IMemberable.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public interface IMemberable : IHaveModifiers 4 | { 5 | [JsonIgnore] 6 | MemberType MemberType { get; } 7 | 8 | [JsonProperty(Order = -3)] 9 | string Name { get; } 10 | 11 | IHaveDocumentationComments? DocumentationComments { get; internal set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/EnumMemberDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("EnumMember {Name,nq}")] 4 | public class EnumMemberDescription(string name, string? value) : MemberDescription(name) 5 | { 6 | public string? Value { get; } = value; 7 | 8 | public override MemberType MemberType => MemberType.EnumMember; 9 | } 10 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/ArgumentDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("Argument {Text} ({Type,nq})")] 4 | public class ArgumentDescription(string type, string text) 5 | { 6 | public string Type { get; } = type ?? throw new ArgumentNullException(nameof(type)); 7 | 8 | public string Text { get; } = text ?? throw new ArgumentNullException(nameof(text)); 9 | } 10 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Render/IEnumerableMethodDescriptionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public static class IEnumerableMethodDescriptionExtensions 4 | { 5 | public static IReadOnlyList WithName(this IEnumerable list, string name) 6 | { 7 | if (list is null) throw new ArgumentNullException(nameof(list)); 8 | 9 | return list.Where(m => string.Equals(m.Name, name, StringComparison.Ordinal)).ToList(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Abstractions/IHaveMethodBody.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public interface IHaveAMethodBody : IHaveModifiers 4 | { 5 | IHaveDocumentationComments? DocumentationComments { get; set; } 6 | 7 | string Name { get; } 8 | 9 | List Parameters { get; } 10 | 11 | [JsonProperty(ItemTypeNameHandling = TypeNameHandling.Objects)] 12 | List Statements { get; } 13 | 14 | List Attributes { get; } 15 | } 16 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Render/IEnumerableStringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public static class IEnumerableStringExtensions 4 | { 5 | public static IReadOnlyList StartsWith(this IEnumerable list, string partialName) 6 | { 7 | if (list is null) throw new ArgumentNullException(nameof(list)); 8 | if (partialName is null) throw new ArgumentNullException(nameof(partialName)); 9 | 10 | return list.Where(bt => bt.StartsWith(partialName, StringComparison.Ordinal)).ToList(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/EventDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("Event {Type,nq} {Name,nq}")] 4 | public class EventDescription(string type, string name) : MemberDescription(name) 5 | { 6 | public string Type { get; } = type ?? throw new ArgumentNullException(nameof(type)); 7 | 8 | public string? Initializer { get; set; } 9 | 10 | [JsonIgnore] 11 | public bool HasInitializer => this.Initializer is not null; 12 | 13 | public override MemberType MemberType => MemberType.Event; 14 | } 15 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/FieldDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("Field {Type,nq} {Name,nq}")] 4 | public class FieldDescription(string type, string name) : MemberDescription(name) 5 | { 6 | public string Type { get; } = type ?? throw new ArgumentNullException(nameof(type)); 7 | 8 | public string? Initializer { get; set; } 9 | 10 | [JsonIgnore] 11 | public bool HasInitializer => this.Initializer is not null; 12 | 13 | public override MemberType MemberType => MemberType.Field; 14 | } 15 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/InvocationDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("Invocation \"{ContainingType,nq}.{Name,nq}\"")] 4 | public class InvocationDescription(string containingType, string name) : Statement 5 | { 6 | public string ContainingType { get; } = containingType ?? throw new ArgumentNullException(nameof(containingType)); 7 | 8 | public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); 9 | 10 | public List Arguments { get; } = []; 11 | } 12 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/AssignmentDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("Assignment \"{Left,nq} {Operator,nq} {Right,nq}\"")] 4 | public class AssignmentDescription(string left, string @operator, string right) : Statement 5 | { 6 | public string Left { get; } = left ?? throw new ArgumentNullException(nameof(left)); 7 | 8 | public string Operator { get; } = @operator ?? throw new ArgumentNullException(nameof(@operator)); 9 | 10 | public string Right { get; } = right ?? throw new ArgumentNullException(nameof(right)); 11 | } 12 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/AttributeArgumentDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("AttributeArgument {Name} {Type} {Value}")] 4 | public class AttributeArgumentDescription(string? name, string? type, string? value) : IAttributeArgumentDescription 5 | { 6 | public string Name { get; } = name ?? throw new ArgumentNullException("name"); 7 | 8 | public string Type { get; } = type ?? throw new ArgumentNullException("type"); 9 | 10 | public string Value { get; } = value ?? throw new ArgumentNullException("value"); 11 | } 12 | -------------------------------------------------------------------------------- /src/LivingDocumentation.UML/Fragments/AltSection.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Uml; 2 | 3 | /// 4 | /// Represents a section in a larger group. 5 | /// 6 | [DebuggerDisplay("AltSection {GroupType} {Label}")] 7 | public class AltSection : InteractionFragment 8 | { 9 | /// 10 | /// The type of group, like alt, else, etc.. 11 | /// 12 | public string? GroupType { get; set; } 13 | 14 | /// 15 | /// The label of this section. 16 | /// 17 | public string? Label { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/PropertyDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("Property {Type,nq} {Name,nq}")] 4 | public class PropertyDescription(string type, string name) : MemberDescription(name) 5 | { 6 | public string Type { get; } = type ?? throw new ArgumentNullException(nameof(type)); 7 | 8 | public string? Initializer { get; set; } 9 | 10 | [JsonIgnore] 11 | public bool HasInitializer => !string.IsNullOrWhiteSpace(this.Initializer); 12 | 13 | public override MemberType MemberType => MemberType.Property; 14 | } 15 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Statements/If.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("If")] 4 | public class If : Statement 5 | { 6 | public List Sections { get; } = []; 7 | 8 | [JsonIgnore] 9 | public override List Statements => this.Sections.SelectMany(s => s.Statements).ToList(); 10 | 11 | [OnDeserialized] 12 | internal new void OnDeserializedMethod(StreamingContext context) 13 | { 14 | foreach (var section in this.Sections) 15 | { 16 | section.Parent ??= this; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Abstractions/Modifier.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [Flags] 4 | public enum Modifier 5 | { 6 | Internal = 1 << 0, 7 | Public = 1 << 1, 8 | Private = 1 << 2, 9 | Protected = 1 << 3, 10 | Static = 1 << 4, 11 | Abstract = 1 << 5, 12 | Override = 1 << 6, 13 | Readonly = 1 << 7, 14 | Async = 1 << 8, 15 | Const = 1 << 9, 16 | Sealed = 1 << 10, 17 | Virtual = 1 << 11, 18 | Extern = 1 << 12, 19 | New = 1 << 13, 20 | Unsafe = 1 << 14, 21 | Partial = 1 << 15, 22 | } 23 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Abstractions/Statement.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public abstract class Statement 4 | { 5 | [JsonProperty(ItemTypeNameHandling = TypeNameHandling.Objects)] 6 | public virtual List Statements { get; } = []; 7 | 8 | [JsonIgnore] 9 | public object? Parent 10 | { 11 | get; internal set; 12 | } 13 | 14 | [OnDeserialized] 15 | internal void OnDeserializedMethod(StreamingContext context) 16 | { 17 | foreach (var statement in this.Statements) 18 | { 19 | statement.Parent ??= this; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Abstractions/IHaveDocumentationComments.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public interface IHaveDocumentationComments 4 | { 5 | string Example { get; } 6 | 7 | Dictionary Exceptions { get; } 8 | 9 | Dictionary Params { get; } 10 | 11 | Dictionary Permissions { get; } 12 | 13 | string Remarks { get; } 14 | 15 | string Returns { get; } 16 | 17 | Dictionary SeeAlsos { get; } 18 | 19 | string Summary { get; } 20 | 21 | Dictionary TypeParams { get; } 22 | 23 | string Value { get; } 24 | } 25 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/DocumentationComments/Section.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.DocumentationComments; 2 | 3 | public class Section 4 | { 5 | public const string Example = "example"; 6 | public const string Exception = "exception"; 7 | public const string Param = "param"; 8 | public const string Permission = "permission"; 9 | public const string Remarks = "remarks"; 10 | public const string Returns = "returns"; 11 | public const string SeeAlso = "seealso"; 12 | public const string Summary = "summary"; 13 | public const string TypeParam = "typeparam"; 14 | public const string Value = "value"; 15 | } 16 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Analyzer/Analyzers/LoopingAnalyzer.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | internal class LoopingAnalyzer(SemanticModel semanticModel, List statements) : CSharpSyntaxWalker 4 | { 5 | public override void VisitForEachStatement(ForEachStatementSyntax node) 6 | { 7 | var forEachStatement = new ForEach(); 8 | statements.Add(forEachStatement); 9 | 10 | forEachStatement.Expression = $"{node.Identifier} in {node.Expression}"; 11 | 12 | var invocationAnalyzer = new InvocationsAnalyzer(semanticModel, forEachStatement.Statements); 13 | invocationAnalyzer.Visit(node.Statement); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/AttributeDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("Attribute {Type} {Name}")] 4 | public class AttributeDescription(string? type, string? name) : IAttributeDescription 5 | { 6 | public string Type { get; } = type ?? throw new ArgumentNullException(nameof(type)); 7 | 8 | public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); 9 | 10 | [JsonProperty(ItemTypeNameHandling = TypeNameHandling.None)] 11 | [JsonConverter(typeof(ConcreteTypeConverter>))] 12 | public List Arguments { get; } = []; 13 | } 14 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Statements/Switch.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("Switch {Expression}")] 4 | public class Switch : Statement 5 | { 6 | public List Sections { get; } = []; 7 | 8 | public string? Expression { get; set; } 9 | 10 | [JsonIgnore] 11 | public override List Statements => this.Sections.SelectMany(s => s.Statements).ToList(); 12 | 13 | [OnDeserialized] 14 | internal new void OnDeserializedMethod(StreamingContext context) 15 | { 16 | foreach (var section in this.Sections) 17 | { 18 | section.Parent ??= this; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Render/IEnumerableIAttributeDescriptionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public static class IEnumerableIAttributeDescriptionExtensions 4 | { 5 | public static IReadOnlyList OfType(this IEnumerable list, string fullname) 6 | { 7 | if (list is null) throw new ArgumentNullException(nameof(list)); 8 | 9 | return list.Where(ad => string.Equals(ad.Type, fullname, StringComparison.Ordinal)).ToList(); 10 | } 11 | 12 | public static bool HasAttribute(this IEnumerable list, string fullname) 13 | { 14 | return list.OfType(fullname).Any(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/ParameterDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("Parameter {Type} {Name}")] 4 | public class ParameterDescription(string type, string name) : IParameterDescription 5 | { 6 | public string Type { get; } = type ?? throw new ArgumentNullException(nameof(type)); 7 | 8 | public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); 9 | 10 | public bool HasDefaultValue { get; set; } 11 | 12 | [JsonProperty(ItemTypeNameHandling = TypeNameHandling.None)] 13 | [JsonConverter(typeof(ConcreteTypeConverter>))] 14 | public List Attributes { get; } = []; 15 | } 16 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/Json/ConcreteTypeConverter.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | internal class ConcreteTypeConverter : JsonConverter 4 | { 5 | public override bool CanConvert(Type objectType) 6 | { 7 | return true; 8 | } 9 | 10 | public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) 11 | { 12 | return serializer.Deserialize(reader); 13 | } 14 | 15 | public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) 16 | { 17 | serializer.TypeNameHandling = TypeNameHandling.None; 18 | 19 | serializer.Serialize(writer, value); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/AnalyzerSetupVerification/TestProject/TestProject.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/LivingDocumentation.UML/Fragments/Alt.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Uml; 2 | 3 | /// 4 | /// Represents a group with 1 or more sections, like alt, par, loop, etc.. 5 | /// 6 | [DebuggerDisplay("Alt")] 7 | public class Alt : InteractionFragment 8 | { 9 | private readonly List sections = []; 10 | 11 | /// 12 | /// Gets all sections. 13 | /// 14 | public IReadOnlyList Sections => this.sections; 15 | 16 | /// 17 | /// Add a sections to this alt. 18 | /// 19 | /// The section to add. 20 | public void AddSection(AltSection section) 21 | { 22 | section.Parent = this; 23 | 24 | this.sections.Add(section); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/LivingDocumentation.UML/IHaveModifiersExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Uml; 2 | 3 | public static class IHaveModifiersExtensions 4 | { 5 | public static VisibilityModifier ToUmlVisibility(this IHaveModifiers modifiers) 6 | { 7 | if (modifiers.IsPublic()) 8 | { 9 | return VisibilityModifier.Public; 10 | } 11 | 12 | if (modifiers.IsInternal()) 13 | { 14 | return VisibilityModifier.PackagePrivate; 15 | } 16 | 17 | if (modifiers.IsProtected()) 18 | { 19 | return VisibilityModifier.Protected; 20 | } 21 | 22 | if (modifiers.IsPrivate()) 23 | { 24 | return VisibilityModifier.Private; 25 | } 26 | 27 | return VisibilityModifier.None; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /samples/LivingDocumentation.eShopOnContainers/LivingDocumentation.Sample.eShopOnContainers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | latest 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/ConstructorDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("Constructor {Name}")] 4 | public class ConstructorDescription(string name) : MemberDescription(name), IHaveAMethodBody 5 | { 6 | [JsonProperty(ItemTypeNameHandling = TypeNameHandling.None)] 7 | [JsonConverter(typeof(ConcreteTypeConverter>))] 8 | public List Parameters { get; } = []; 9 | 10 | public List Statements { get; } = []; 11 | 12 | public override MemberType MemberType => MemberType.Constructor; 13 | 14 | [OnDeserialized] 15 | internal void OnDeserializedMethod(StreamingContext context) 16 | { 17 | foreach (var statement in this.Statements) 18 | { 19 | statement.Parent = this; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/LivingDocumentation.UML/Fragments/Arrow.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Uml; 2 | 3 | /// 4 | /// Represents an arrow between 2 participants. 5 | /// 6 | [DebuggerDisplay("{Source} -> {Target} : {Name}")] 7 | public class Arrow : InteractionFragment 8 | { 9 | /// 10 | /// The left participant of the arrow. 11 | /// 12 | public string? Source { get; set; } 13 | 14 | /// 15 | /// The right participant of the arrow. 16 | /// 17 | public string? Target { get; set; } 18 | 19 | /// 20 | /// The optional color of the arrow. 21 | /// 22 | public string? Color { get; set; } 23 | 24 | /// 25 | /// Whether the arrow is dashed. 26 | /// 27 | public bool Dashed { get; set; } 28 | 29 | /// 30 | /// The message with the arrow. 31 | /// 32 | public string? Name { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/MethodDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("Method {ReturnType,nq} {Name,nq}")] 4 | public class MethodDescription(string? returnType, string name) : MemberDescription(name), IHaveAMethodBody 5 | { 6 | [DefaultValue("void")] 7 | public string ReturnType { get; } = returnType ?? "void"; 8 | 9 | [JsonProperty(ItemTypeNameHandling = TypeNameHandling.None)] 10 | [JsonConverter(typeof(ConcreteTypeConverter>))] 11 | public List Parameters { get; } = []; 12 | 13 | public List Statements { get; } = []; 14 | 15 | public override MemberType MemberType => MemberType.Method; 16 | 17 | [OnDeserialized] 18 | internal void OnDeserializedMethod(StreamingContext context) 19 | { 20 | foreach (var statement in this.Statements) 21 | { 22 | statement.Parent = this; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Descriptions.Tests/MethodDescriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Description.Tests; 2 | 3 | [TestClass] 4 | public class MethodDescriptionTests 5 | { 6 | [TestMethod] 7 | public void ConstructorShouldSetTypesCorrectly() 8 | { 9 | // Act 10 | var description = new MethodDescription("Type", "Name"); 11 | 12 | // Assert 13 | description.MemberType.Should().Be(MemberType.Method); 14 | description.IsInherited.Should().BeFalse(); 15 | description.ReturnType.Should().Be("Type"); 16 | description.Name.Should().Be("Name"); 17 | description.Parameters.Should().BeEmpty(); 18 | description.Statements.Should().BeEmpty(); 19 | } 20 | 21 | [TestMethod] 22 | public void ReturnTypeShouldBeVoidWhenNull() 23 | { 24 | // Act 25 | var description = new MethodDescription(null, "Name"); 26 | 27 | // Assert 28 | description.ReturnType.Should().Be("void"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Json/JsonDefaults.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public static class JsonDefaults 4 | { 5 | public static JsonSerializerSettings SerializerSettings() 6 | { 7 | var serializerSettings = new JsonSerializerSettings 8 | { 9 | DefaultValueHandling = DefaultValueHandling.Ignore, 10 | NullValueHandling = NullValueHandling.Ignore, 11 | ContractResolver = new SkipEmptyCollectionsContractResolver(), 12 | TypeNameHandling = TypeNameHandling.Auto 13 | }; 14 | 15 | return serializerSettings; 16 | } 17 | 18 | public static JsonSerializerSettings DeserializerSettings() 19 | { 20 | var serializerSettings = new JsonSerializerSettings 21 | { 22 | DefaultValueHandling = DefaultValueHandling.Ignore, 23 | ContractResolver = new SkipEmptyCollectionsContractResolver(), 24 | TypeNameHandling = TypeNameHandling.Auto 25 | }; 26 | 27 | return serializerSettings; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /samples/LivingDocumentation.eShopOnContainers/Program.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | 6 | namespace LivingDocumentation.eShopOnContainers 7 | { 8 | public class Program 9 | { 10 | public static List Types; 11 | 12 | public static async Task Main(string[] args) 13 | { 14 | // Read analysis 15 | var result = await File.ReadAllTextAsync("analysis.json"); 16 | 17 | Types = JsonConvert.DeserializeObject>(result, JsonDefaults.DeserializerSettings()); 18 | 19 | var aggregateFiles = new AggregateRenderer().Render(); 20 | var commandHandlerFiles = new CommandHandlerRenderer().Render(); 21 | var eventHandlerFiles = new EventHandlerRenderer().Render(); 22 | 23 | new AsciiDocRenderer(Types, aggregateFiles, commandHandlerFiles, eventHandlerFiles).Render(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public static class StringExtensions 4 | { 5 | private const char Dot = '.'; 6 | 7 | public static string ClassName(this string fullName) 8 | { 9 | if (fullName is null) 10 | { 11 | return string.Empty; 12 | } 13 | 14 | return fullName.Substring(Math.Min(fullName.LastIndexOf(Dot) + 1, fullName.Length)); 15 | } 16 | 17 | public static string Namespace(this string fullName) 18 | { 19 | if (fullName is null) 20 | { 21 | return string.Empty; 22 | } 23 | 24 | return fullName.Substring(0, Math.Max(fullName.LastIndexOf(Dot), 0)).Trim(Dot); 25 | } 26 | 27 | public static IReadOnlyList NamespaceParts(this string fullName) 28 | { 29 | if (fullName is null) 30 | { 31 | return new List(0); 32 | } 33 | 34 | return fullName.Split(new[] { Dot }, StringSplitOptions.RemoveEmptyEntries); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Analyzer/Extensions/SemanticModelExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public static class SemanticModelExtensions 4 | { 5 | public static string GetTypeDisplayString(this SemanticModel semanticModel, SyntaxNode node) 6 | { 7 | return semanticModel.GetTypeInfo(node).Type.ToDisplayString(); 8 | } 9 | 10 | public static string GetTypeDisplayString(this SemanticModel semanticModel, ExpressionSyntax expression) 11 | { 12 | var type = semanticModel.GetTypeInfo(expression).Type?.ToDisplayString(); 13 | if (type is not null) 14 | { 15 | return type; 16 | } 17 | 18 | type = semanticModel.GetTypeInfo(expression).ConvertedType?.ToDisplayString(); 19 | if (type is not null) 20 | { 21 | return type; 22 | } 23 | 24 | return semanticModel.GetCollectionInitializerSymbolInfo(expression).Symbol?.ContainingType.ToDisplayString() ?? throw new KeyNotFoundException($"Could not resolve a display name for {expression}"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Descriptions.Tests/ArgumentDescriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Description.Tests; 2 | 3 | [TestClass] 4 | public class ArgumentDescriptionTests 5 | { 6 | [TestMethod] 7 | public void ConstructorShouldSetTypesCorrectly() 8 | { 9 | // Act 10 | var description = new ArgumentDescription("Type", "Text"); 11 | 12 | // Assert 13 | description.Type.Should().Be("Type"); 14 | description.Text.Should().Be("Text"); 15 | } 16 | 17 | [DataRow(null, "Text", "type", DisplayName = "Constuctor should throw when `type` is `null`")] 18 | [DataRow("Type", null, "text", DisplayName = "Constuctor should throw when `text` is `null`")] 19 | [TestMethod] 20 | public void ConstructorShouldGuardAgainstNullParamters(string type, string text, string parameterName) 21 | { 22 | // Act 23 | Action act = () => new ArgumentDescription(type, text); 24 | 25 | // Assert 26 | act.Should().ThrowExactly() 27 | .WithParameterName(parameterName); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Descriptions.Tests/EnumMemberDescriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Description.Tests; 2 | 3 | [TestClass] 4 | public class EnumMemberDescriptionTests 5 | { 6 | [TestMethod] 7 | public void ConstructorShouldSetTypesCorrectly() 8 | { 9 | // Act 10 | var description = new EnumMemberDescription("Name", "Value"); 11 | 12 | // Assert 13 | description.MemberType.Should().Be(MemberType.EnumMember); 14 | description.IsInherited.Should().BeFalse(); 15 | description.Name.Should().Be("Name"); 16 | description.Value.Should().Be("Value"); 17 | } 18 | 19 | [DataRow(null, null, "name", DisplayName = "Constuctor should throw when `name` is `null`")] 20 | [TestMethod] 21 | public void ConstructorShouldGuardAgainstNullParamters(string name, string value, string parameterName) 22 | { 23 | // Act 24 | Action act = () => new EnumMemberDescription(name, value); 25 | 26 | // Assert 27 | act.Should().ThrowExactly() 28 | .WithParameterName(parameterName); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LivingDocumentation.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | lcov,cobertura 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | .*\.dll$ 16 | 17 | 18 | .*\.Tests\.dll$ 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Michaël Hompus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Descriptions.Tests/AttributeDescriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Description.Tests; 2 | 3 | [TestClass] 4 | public class AttributeDescriptionTests 5 | { 6 | [TestMethod] 7 | public void ConstructorShouldSetTypesCorrectly() 8 | { 9 | // Act 10 | var description = new AttributeDescription("Type", "Name"); 11 | 12 | // Assert 13 | description.Type.Should().Be("Type"); 14 | description.Name.Should().Be("Name"); 15 | description.Arguments.Should().BeEmpty(); 16 | } 17 | 18 | [DataRow(null, "Text", "type", DisplayName = "Constuctor should throw when `type` is `null`")] 19 | [DataRow("Type", null, "name", DisplayName = "Constuctor should throw when `name` is `null`")] 20 | [TestMethod] 21 | public void ConstructorShouldGuardAgainstNullParamters(string type, string name, string parameterName) 22 | { 23 | // Act 24 | Action act = () => new AttributeDescription(type, name); 25 | 26 | // Assert 27 | act.Should().ThrowExactly() 28 | .WithParameterName(parameterName); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Descriptions.Tests/ConstructureDescriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Description.Tests; 2 | 3 | [TestClass] 4 | public class ConstructorDescriptionTests 5 | { 6 | [TestMethod] 7 | public void ConstructorShouldSetTypesCorrectly() 8 | { 9 | // Act 10 | var description = new ConstructorDescription("Name"); 11 | 12 | // Assert 13 | description.MemberType.Should().Be(MemberType.Constructor); 14 | description.IsInherited.Should().BeFalse(); 15 | description.Name.Should().Be("Name"); 16 | description.Parameters.Should().BeEmpty(); 17 | description.Statements.Should().BeEmpty(); 18 | } 19 | 20 | [DataRow(null, "name", DisplayName = "Constuctor should throw when `name` is `null`")] 21 | [TestMethod] 22 | public void ConstructorShouldGuardAgainstNullParamters(string name, string parameterName) 23 | { 24 | // Act 25 | Action act = () => new ConstructorDescription(name); 26 | 27 | // Assert 28 | act.Should().ThrowExactly() 29 | .WithParameterName(parameterName); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Descriptions.Tests/InvocationDescriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Description.Tests; 2 | 3 | [TestClass] 4 | public class InvocationDescriptionTests 5 | { 6 | [TestMethod] 7 | public void ConstructorShouldSetTypesCorrectly() 8 | { 9 | // Act 10 | var description = new InvocationDescription("Type", "Name"); 11 | 12 | // Assert 13 | description.ContainingType.Should().Be("Type"); 14 | description.Name.Should().Be("Name"); 15 | description.Arguments.Should().BeEmpty(); 16 | } 17 | 18 | [DataRow(null, "Name", "containingType", DisplayName = "Constuctor should throw when `containingType` is `null`")] 19 | [DataRow("Type", null, "name", DisplayName = "Constuctor should throw when `name` is `null`")] 20 | [TestMethod] 21 | public void ConstructorShouldGuardAgainstNullParamters(string containingType, string name, string parameterName) 22 | { 23 | // Act 24 | Action act = () => new InvocationDescription(containingType, name); 25 | 26 | // Assert 27 | act.Should().ThrowExactly() 28 | .WithParameterName(parameterName); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Descriptions.Tests/ParameterDescriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Description.Tests; 2 | 3 | [TestClass] 4 | public class ParameterDescriptionTests 5 | { 6 | [TestMethod] 7 | public void ConstructorShouldSetTypesCorrectly() 8 | { 9 | // Act 10 | var description = new ParameterDescription("Type", "Name"); 11 | 12 | // Assert 13 | description.Type.Should().Be("Type"); 14 | description.Name.Should().Be("Name"); 15 | description.HasDefaultValue.Should().BeFalse(); 16 | description.Attributes.Should().BeEmpty(); 17 | } 18 | 19 | [DataRow(null, "Name", "type", DisplayName = "Constuctor should throw when `type` is `null`")] 20 | [DataRow("Type", null, "name", DisplayName = "Constuctor should throw when `name` is `null`")] 21 | [TestMethod] 22 | public void ConstructorShouldGuardAgainstNullParamters(string type, string name, string parameterName) 23 | { 24 | // Act 25 | Action act = () => new ParameterDescription(type, name); 26 | 27 | // Assert 28 | act.Should().ThrowExactly() 29 | .WithParameterName(parameterName); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Analyzer.Tests/PartialClassTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Analyzer.Tests; 2 | 3 | [TestClass] 4 | public class PartialClassTests 5 | { 6 | [TestMethod] 7 | public void PartialClassesShouldBecomeASingleType() 8 | { 9 | // Assign 10 | var source = @" 11 | partial class Test 12 | { 13 | public string Property1 { get; } 14 | } 15 | 16 | partial class Test 17 | { 18 | public string Property2 { get; } 19 | } 20 | "; 21 | 22 | // Act 23 | var types = TestHelper.VisitSyntaxTree(source); 24 | 25 | // Assert 26 | types.Should().HaveCount(1); 27 | } 28 | 29 | [TestMethod] 30 | public void MembersOfPartialClassesShouldBeCombined() 31 | { 32 | // Assign 33 | var source = @" 34 | partial class Test 35 | { 36 | public string Property1 { get; } 37 | } 38 | 39 | partial class Test 40 | { 41 | public string Property2 { get; } 42 | } 43 | "; 44 | 45 | // Act 46 | var types = TestHelper.VisitSyntaxTree(source); 47 | 48 | // Assert 49 | types[0].Properties.Should().HaveCount(2); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Analyzer.Tests/LivingDocumentation.Analyzer.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | true 7 | latest 8 | 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Descriptions.Tests/LivingDocumentation.Descriptions.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | true 7 | latest 8 | 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Descriptions.Tests/AssignmentDescriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Description.Tests; 2 | 3 | [TestClass] 4 | public class AssignmentDescriptionTests 5 | { 6 | [TestMethod] 7 | public void ConstructorShouldSetTypesCorrectly() 8 | { 9 | // Act 10 | var description = new AssignmentDescription("Left", "=", "Right"); 11 | 12 | // Assert 13 | description.Left.Should().Be("Left"); 14 | description.Operator.Should().Be("="); 15 | description.Right.Should().Be("Right"); 16 | } 17 | 18 | [DataRow(null, "=", "Right", "left", DisplayName = "Constuctor should throw when `left` is `null`")] 19 | [DataRow("Left", null, "Right", "operator", DisplayName = "Constuctor should throw when `operator` is `null`")] 20 | [DataRow("Left", "=", null, "right", DisplayName = "Constuctor should throw when `right` is `null`")] 21 | [TestMethod] 22 | public void ConstructorShouldGuardAgainstNullParamters(string left, string @operator, string right, string parameterName) 23 | { 24 | // Act 25 | Action act = () => new AssignmentDescription(left, @operator, right); 26 | 27 | // Assert 28 | act.Should().ThrowExactly() 29 | .WithParameterName(parameterName); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Descriptions.Tests/AttributeArgumentDescriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Description.Tests; 2 | 3 | [TestClass] 4 | public class AttributeArgumentDescriptionTests 5 | { 6 | [TestMethod] 7 | public void ConstructorShouldSetTypesCorrectly() 8 | { 9 | // Act 10 | var description = new AttributeArgumentDescription("Name", "Type", "Value"); 11 | 12 | // Assert 13 | description.Name.Should().Be("Name"); 14 | description.Type.Should().Be("Type"); 15 | description.Value.Should().Be("Value"); 16 | } 17 | 18 | [DataRow(null, "=", "Value", "name", DisplayName = "Constuctor should throw when `name` is `null`")] 19 | [DataRow("Name", null, "Value", "type", DisplayName = "Constuctor should throw when `type` is `null`")] 20 | [DataRow("Name", "=", null, "value", DisplayName = "Constuctor should throw when `value` is `null`")] 21 | [TestMethod] 22 | public void ConstructorShouldGuardAgainstNullParamters(string name, string type, string value, string parameterName) 23 | { 24 | // Act 25 | Action act = () => new AttributeArgumentDescription(name, type, value); 26 | 27 | // Assert 28 | act.Should().ThrowExactly() 29 | .WithParameterName(parameterName); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Serializations.Tests/LivingDocumentation.Serializations.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | true 7 | latest 8 | 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Analyzer/Options.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public partial class Program 4 | { 5 | public class Options 6 | { 7 | [Option("solution", Required = true, SetName = "solution", HelpText = "The solution to analyze.")] 8 | public string? SolutionPath { get; set; } 9 | 10 | [Option("project", Required = true, SetName = "project", HelpText = "The project to analyze.")] 11 | public string? ProjectPath { get; set; } 12 | 13 | [Option("exclude", Required = false, SetName = "solution", Separator = ',', HelpText = "Any projects to exclude from analysis.")] 14 | public IEnumerable ExcludedProjectPaths { get; set; } = Enumerable.Empty(); 15 | 16 | [Option("output", Required = true, HelpText = "The location of the output.")] 17 | public string? OutputPath { get; set; } 18 | 19 | [Option('v', "verbose", Default = false, HelpText = "Show warnings during compilation.")] 20 | public bool VerboseOutput { get; set; } 21 | 22 | [Option('p', "pretty", Default = false, HelpText = "Store JSON output in indented formatting.")] 23 | public bool PrettyPrint { get; set; } 24 | 25 | [Option('q', "quiet", Default = false, HelpText = "Don't output informational messages.")] 26 | public bool Quiet { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/LivingDocumentation.UML/Fragments/InteractionFragment.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Uml; 2 | 3 | /// 4 | /// Represents interaction fragments in a sequence diagram. 5 | /// 6 | public abstract class InteractionFragment 7 | { 8 | private readonly List interactionFragments = []; 9 | 10 | /// 11 | /// The parent of this fragment. 12 | /// 13 | public InteractionFragment? Parent { get; set; } 14 | 15 | /// 16 | /// The children of this fragment. 17 | /// 18 | public virtual IReadOnlyList Fragments => this.interactionFragments; 19 | 20 | /// 21 | /// Add a fragment to this level. 22 | /// 23 | /// The fragment to add. 24 | public void AddFragment(InteractionFragment fragment) 25 | { 26 | fragment.Parent = this; 27 | 28 | this.interactionFragments.Add(fragment); 29 | } 30 | 31 | /// 32 | /// Add a list of fragments to this level. 33 | /// 34 | /// The fragments to add. 35 | public void AddFragments(IEnumerable fragments) 36 | { 37 | foreach (var fragment in fragments) 38 | { 39 | this.AddFragment(fragment); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Analyzer.Tests/EnumModifierTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Analyzer.Tests; 2 | 3 | [TestClass] 4 | public class EnumModifierTests 5 | { 6 | [TestMethod] 7 | public void EnumWithoutModifier_Should_HaveDefaultInternalModifier() 8 | { 9 | // Assign 10 | var source = @" 11 | enum Test 12 | { 13 | } 14 | "; 15 | 16 | // Act 17 | var types = TestHelper.VisitSyntaxTree(source); 18 | 19 | // Assert 20 | types[0].Modifiers.Should().Be(Modifier.Internal); 21 | } 22 | 23 | [TestMethod] 24 | public void PublicEnum_Should_HavePublicModifier() 25 | { 26 | // Assign 27 | var source = @" 28 | public enum Test 29 | { 30 | } 31 | "; 32 | 33 | // Act 34 | var types = TestHelper.VisitSyntaxTree(source); 35 | 36 | // Assert 37 | types[0].Modifiers.Should().Be(Modifier.Public); 38 | } 39 | 40 | [TestMethod] 41 | public void EnumMembers_Should_HavePublicModifier() 42 | { 43 | // Assign 44 | var source = @" 45 | public enum Test 46 | { 47 | Value 48 | } 49 | "; 50 | 51 | // Act 52 | var types = TestHelper.VisitSyntaxTree(source); 53 | 54 | // Assert 55 | types[0].EnumMembers[0].Modifiers.Should().Be(Modifier.Public); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Extensions.Tests/LivingDocumentation.Extensions.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | true 7 | latest 8 | 9 | false 10 | 11 | false 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/MemberDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public abstract class MemberDescription(string name) : IMemberable 4 | { 5 | public abstract MemberType MemberType { get; } 6 | 7 | public string Name { get; } = name ?? throw new ArgumentNullException("name"); 8 | 9 | [DefaultValue(Modifier.Private)] 10 | [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] 11 | public Modifier Modifiers { get; set; } 12 | 13 | [JsonIgnore] 14 | public bool IsInherited { get; internal set; } = false; 15 | 16 | [JsonConverter(typeof(ConcreteTypeConverter))] 17 | public IHaveDocumentationComments? DocumentationComments { get; set; } 18 | 19 | [JsonProperty(ItemTypeNameHandling = TypeNameHandling.None)] 20 | [JsonConverter(typeof(ConcreteTypeConverter>))] 21 | public List Attributes { get; } = []; 22 | 23 | public override bool Equals(object? obj) 24 | { 25 | if (obj is not MemberDescription other) 26 | { 27 | return false; 28 | } 29 | 30 | return Equals(this.MemberType, other.MemberType) && string.Equals(this.Name, other.Name); 31 | } 32 | 33 | public override int GetHashCode() 34 | { 35 | return (this.MemberType, this.Name).GetHashCode(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.RenderExtensions.Tests/LivingDocumentation.RenderExtensions.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | true 7 | latest 8 | 9 | false 10 | 11 | false 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Analyzer.Tests/ForEachTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Analyzer.Tests; 2 | 3 | [TestClass] 4 | public class ForEachTests 5 | { 6 | [TestMethod] 7 | public void ForEach_Should_BeDetected() 8 | { 9 | // Assign 10 | var source = @" 11 | class Test 12 | { 13 | void Method() 14 | { 15 | var items = new string[0]; 16 | foreach (var item in items) 17 | { 18 | } 19 | } 20 | } 21 | "; 22 | 23 | // Act 24 | var types = TestHelper.VisitSyntaxTree(source); 25 | 26 | // Assert 27 | types[0].Methods[0].Statements[0].Should().BeOfType(); 28 | } 29 | 30 | [TestMethod] 31 | public void ForEachStatements_Should_BeParsed() 32 | { 33 | // Assign 34 | var source = @" 35 | class Test 36 | { 37 | void Method() 38 | { 39 | var items = new string[0]; 40 | foreach (var item in items) 41 | { 42 | var o = new System.Object(); 43 | } 44 | } 45 | } 46 | "; 47 | 48 | // Act 49 | var types = TestHelper.VisitSyntaxTree(source); 50 | 51 | // Assert 52 | var forEach = types[0].Methods[0].Statements[0]; 53 | forEach.Statements.Should().HaveCount(1); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Serializations.Tests/TestHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CSharp; 6 | 7 | namespace LivingDocumentation.Analyzer.Tests 8 | { 9 | internal class TestHelper 10 | { 11 | public static IReadOnlyList VisitSyntaxTree(string source) 12 | { 13 | source.Should().NotBeNullOrWhiteSpace("without source code there is nothing to test"); 14 | 15 | var syntaxTree = CSharpSyntaxTree.ParseText(source.Trim()); 16 | var compilation = CSharpCompilation.Create("Test") 17 | .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) 18 | .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) 19 | .AddSyntaxTrees(syntaxTree); 20 | 21 | var diagnostics = compilation.GetDiagnostics(); 22 | diagnostics.Should().HaveCount(0, "there shoudn't be any compile errors"); 23 | 24 | var semanticModel = compilation.GetSemanticModel(syntaxTree, true); 25 | 26 | var types = new List(); 27 | 28 | var visitor = new SourceAnalyzer(semanticModel, types); 29 | visitor.Visit(syntaxTree.GetRoot()); 30 | 31 | return types; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Render/InvocationDescriptionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public static class InvocationDescriptionExtensions 4 | { 5 | public static bool MatchesMethod(this InvocationDescription invocation, IHaveAMethodBody method) 6 | { 7 | if (invocation is null) throw new ArgumentNullException(nameof(invocation)); 8 | if (method is null) throw new ArgumentNullException(nameof(method)); 9 | 10 | return string.Equals(invocation.Name, method.Name) && invocation.MatchesParameters(method); 11 | } 12 | 13 | public static bool MatchesParameters(this InvocationDescription invocation, IHaveAMethodBody method) 14 | { 15 | if (invocation is null) throw new ArgumentNullException(nameof(invocation)); 16 | if (method is null) throw new ArgumentNullException(nameof(method)); 17 | 18 | if (invocation.Arguments.Count == 0) 19 | { 20 | return method.Parameters.Count == 0; 21 | } 22 | 23 | var invokedWithTypes = invocation.Arguments.Select(a => a.Type).ToList(); 24 | if (invokedWithTypes.Count > method.Parameters.Count) 25 | { 26 | return false; 27 | } 28 | 29 | var optionalArguments = method.Parameters.Count(p => p.HasDefaultValue); 30 | if (optionalArguments == 0) 31 | { 32 | return method.Parameters.Select(p => p.Type).SequenceEqual(invokedWithTypes); 33 | } 34 | 35 | return method.Parameters.Take(invokedWithTypes.Count).Select(p => p.Type).SequenceEqual(invokedWithTypes); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Analyzer.Tests/TestHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | 4 | namespace LivingDocumentation.Analyzer.Tests; 5 | 6 | internal class TestHelper 7 | { 8 | public static IReadOnlyList VisitSyntaxTree(string source, params string[] ignoreErrorCodes) 9 | { 10 | source.Should().NotBeNullOrWhiteSpace("without source code there is nothing to test"); 11 | 12 | var syntaxTree = CSharpSyntaxTree.ParseText(source.Trim()); 13 | var compilation = CSharpCompilation.Create("Test") 14 | .WithOptions( 15 | new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) 16 | .WithAllowUnsafe(true) 17 | ) 18 | .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) 19 | .AddSyntaxTrees(syntaxTree); 20 | 21 | var diagnostics = compilation.GetDiagnostics().Where(d => !ignoreErrorCodes.Contains(d.Id)); 22 | diagnostics.Should().HaveCount(0, "there shoudn't be any compile errors"); 23 | 24 | var semanticModel = compilation.GetSemanticModel(syntaxTree, true); 25 | 26 | var types = new List(); 27 | 28 | var visitor = new SourceAnalyzer(semanticModel, types); 29 | visitor.Visit(syntaxTree.GetRoot()); 30 | 31 | return types; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Descriptions.Tests/EventDescriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Description.Tests; 2 | 3 | [TestClass] 4 | public class EventDescriptionTests 5 | { 6 | [TestMethod] 7 | public void ConstructorShouldSetTypesCorrectly() 8 | { 9 | // Act 10 | var description = new EventDescription("Type", "Name"); 11 | 12 | // Assert 13 | description.MemberType.Should().Be(MemberType.Event); 14 | description.IsInherited.Should().BeFalse(); 15 | description.Type.Should().Be("Type"); 16 | description.Name.Should().Be("Name"); 17 | description.HasInitializer.Should().BeFalse(); 18 | } 19 | 20 | [DataRow(null, "Name", "type", DisplayName = "Constuctor should throw when `type` is `null`")] 21 | [DataRow("Type", null, "name", DisplayName = "Constuctor should throw when `name` is `null`")] 22 | [TestMethod] 23 | public void ConstructorShouldGuardAgainstNullParamters(string type, string name, string parameterName) 24 | { 25 | // Act 26 | Action act = () => new EventDescription(type, name); 27 | 28 | // Assert 29 | act.Should().ThrowExactly() 30 | .WithParameterName(parameterName); 31 | } 32 | 33 | [TestMethod] 34 | public void InitializerShouldBeSetCorrectly() 35 | { 36 | // Act 37 | var description = new EventDescription("Type", "Name") 38 | { 39 | Initializer = "1" 40 | }; 41 | 42 | // Assert 43 | description.Initializer.Should().Be("1"); 44 | description.HasInitializer.Should().BeTrue(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Descriptions.Tests/FieldDescriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Description.Tests; 2 | 3 | [TestClass] 4 | public class FieldDescriptionTests 5 | { 6 | [TestMethod] 7 | public void ConstructorShouldSetTypesCorrectly() 8 | { 9 | // Act 10 | var description = new FieldDescription("Type", "Name"); 11 | 12 | // Assert 13 | description.MemberType.Should().Be(MemberType.Field); 14 | description.IsInherited.Should().BeFalse(); 15 | description.Type.Should().Be("Type"); 16 | description.Name.Should().Be("Name"); 17 | description.HasInitializer.Should().BeFalse(); 18 | } 19 | 20 | [DataRow(null, "Name", "type", DisplayName = "Constuctor should throw when `type` is `null`")] 21 | [DataRow("Type", null, "name", DisplayName = "Constuctor should throw when `name` is `null`")] 22 | [TestMethod] 23 | public void ConstructorShouldGuardAgainstNullParamters(string type, string name, string parameterName) 24 | { 25 | // Act 26 | Action act = () => new FieldDescription(type, name); 27 | 28 | // Assert 29 | act.Should().ThrowExactly() 30 | .WithParameterName(parameterName); 31 | } 32 | 33 | [TestMethod] 34 | public void InitializerShouldBeSetCorrectly() 35 | { 36 | // Act 37 | var description = new FieldDescription("Type", "Name") 38 | { 39 | Initializer = "1" 40 | }; 41 | 42 | // Assert 43 | description.Initializer.Should().Be("1"); 44 | description.HasInitializer.Should().BeTrue(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Descriptions.Tests/DocumentationComments/DocumentationCommentsDescriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Description.Tests; 2 | 3 | [TestClass] 4 | public class DocumentationCommentsDescriptionTests 5 | { 6 | [TestMethod] 7 | public void DefaultDocumentationSummary_Should_NotBeNull() 8 | { 9 | // Assign 10 | var documentation = new DocumentationCommentsDescription(); 11 | 12 | // Assert 13 | documentation.Summary.Should().NotBeNull(); 14 | } 15 | 16 | [TestMethod] 17 | public void DefaultDocumentationReturns_Should_NotBeNull() 18 | { 19 | // Assign 20 | var documentation = new DocumentationCommentsDescription(); 21 | 22 | // Assert 23 | documentation.Returns.Should().NotBeNull(); 24 | } 25 | 26 | [TestMethod] 27 | public void DefaultDocumentationRemarks_Should_NotBeNull() 28 | { 29 | // Assign 30 | var documentation = new DocumentationCommentsDescription(); 31 | 32 | // Assert 33 | documentation.Remarks.Should().NotBeNull(); 34 | } 35 | 36 | [TestMethod] 37 | public void DefaultDocumentationValue_Should_NotBeNull() 38 | { 39 | // Assign 40 | var documentation = new DocumentationCommentsDescription(); 41 | 42 | // Assert 43 | documentation.Value.Should().NotBeNull(); 44 | } 45 | 46 | [TestMethod] 47 | public void DefaultDocumentationExample_Should_NotBeNull() 48 | { 49 | // Assign 50 | var documentation = new DocumentationCommentsDescription(); 51 | 52 | // Assert 53 | documentation.Example.Should().NotBeNull(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Extensions/LivingDocumentation.Extensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | latest 7 | true 8 | enable 9 | 10 | Michaël Hompus 11 | https://github.com/eNeRGy164/LivingDocumentation 12 | Copyright Michaël Hompus 2019 13 | https://github.com/eNeRGy164/LivingDocumentation 14 | git 15 | Living Documentation; LivingDocumentation; Roslyn; UML; Generation; 16 | en-US 17 | 0.1.0 18 | MIT 19 | 20 | true 21 | true 22 | true 23 | true 24 | snupkg 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.RenderExtensions.Tests/IEnumerableMethodDescriptionExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.RenderExtensions.Tests; 2 | 3 | [TestClass] 4 | public class IEnumerableMethodDescriptionExtensionsTests 5 | { 6 | [TestMethod] 7 | public void ExtensionMethodShouldGuardAgainstNRE() 8 | { 9 | // Assign 10 | IEnumerable list = default; 11 | 12 | // Act 13 | Action act = () => list.WithName(""); 14 | 15 | // Assert 16 | act.Should().ThrowExactly() 17 | .WithParameterName("list"); 18 | } 19 | 20 | [DataRow("Method1", 1, DisplayName = "When the method exists in the list, return the exact match")] 21 | [DataRow("Method3", 2, DisplayName = "When the method exists multiple times in the list, return all matches")] 22 | [DataRow("Method4", 0, DisplayName = "When the method does not exists in the list, return no matches")] 23 | [DataRow("method1", 0, DisplayName = "When the method exists with a different casing in the list, return no matches")] 24 | [TestMethod] 25 | public void ExpectTheFilterToBeAppliedCorrectlyOnTheList(string value, int expectation) 26 | { 27 | // Assign 28 | var list = new List() 29 | { 30 | new MethodDescription("void", "Method1"), 31 | new MethodDescription("void", "Method2"), 32 | new MethodDescription("void", "Method3"), 33 | new MethodDescription("void", "Method3") 34 | }; 35 | 36 | // Act 37 | var result = list.WithName(value); 38 | 39 | // Assert 40 | result.Should().HaveCount(expectation); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Descriptions.Tests/PropertyDescriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Description.Tests; 2 | 3 | [TestClass] 4 | public class PropertyDescriptionTests 5 | { 6 | [TestMethod] 7 | public void ConstructorShouldSetTypesCorrectly() 8 | { 9 | // Act 10 | var description = new PropertyDescription("Type", "Name"); 11 | 12 | // Assert 13 | description.MemberType.Should().Be(MemberType.Property); 14 | description.IsInherited.Should().BeFalse(); 15 | description.Type.Should().Be("Type"); 16 | description.Name.Should().Be("Name"); 17 | description.HasInitializer.Should().BeFalse(); 18 | description.Attributes.Should().BeEmpty(); 19 | } 20 | 21 | [DataRow(null, "Name", "type", DisplayName = "Constuctor should throw when `type` is `null`")] 22 | [DataRow("Type", null, "name", DisplayName = "Constuctor should throw when `name` is `null`")] 23 | [TestMethod] 24 | public void ConstructorShouldGuardAgainstNullParamters(string type, string name, string parameterName) 25 | { 26 | // Act 27 | Action act = () => new PropertyDescription(type, name); 28 | 29 | // Assert 30 | act.Should().ThrowExactly() 31 | .WithParameterName(parameterName); 32 | } 33 | 34 | [TestMethod] 35 | public void InitializerShouldBeSetCorrectly() 36 | { 37 | // Act 38 | var description = new PropertyDescription("Type", "Name") 39 | { 40 | Initializer = "1" 41 | }; 42 | 43 | // Assert 44 | description.Initializer.Should().Be("1"); 45 | description.HasInitializer.Should().BeTrue(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Json/SkipEmptyCollectionsContractResolver.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | /// 4 | /// Code based on example by Discord [http://stackoverflow.com/a/18486790] 5 | /// > 6 | public class SkipEmptyCollectionsContractResolver : DefaultContractResolver 7 | { 8 | protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) 9 | { 10 | var property = base.CreateProperty(member, memberSerialization); 11 | 12 | var isDefaultValueIgnored = ((property.DefaultValueHandling ?? DefaultValueHandling.Ignore) & DefaultValueHandling.Ignore) != 0; 13 | if (!isDefaultValueIgnored || typeof(string).IsAssignableFrom(property.PropertyType) || !typeof(IEnumerable).IsAssignableFrom(property.PropertyType)) 14 | { 15 | // The property should not be ignored, or is of the type String or is not of the type IEnumerable 16 | // Just return the property 17 | return property; 18 | } 19 | 20 | // This check return true if the collection contains items 21 | bool newShouldSerialize(object obj) 22 | { 23 | return property.ValueProvider.GetValue(obj) is not ICollection collection || collection.Count > 0; 24 | } 25 | 26 | var originalShouldSerialize = property.ShouldSerialize; 27 | 28 | // The property should serialize if the original check (if any) and the new check both incicate the value should be serialized 29 | property.ShouldSerialize = originalShouldSerialize != null ? o => originalShouldSerialize(o) && newShouldSerialize(o) : newShouldSerialize; 30 | 31 | return property; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Json/LivingDocumentation.Json.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | LivingDocumentation 7 | 8 | latest 9 | true 10 | enable 11 | 12 | Michaël Hompus 13 | https://github.com/eNeRGy164/LivingDocumentation 14 | Copyright Michaël Hompus 2019 15 | https://github.com/eNeRGy164/LivingDocumentation 16 | git 17 | Living Documentation; LivingDocumentation; Roslyn; UML; Generation; 18 | en-US 19 | 0.1.0 20 | MIT 21 | 22 | true 23 | true 24 | true 25 | true 26 | snupkg 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Render/LivingDocumentation.RenderExtensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | LivingDocumentation 7 | 8 | latest 9 | true 10 | enable 11 | 12 | Michaël Hompus 13 | https://github.com/eNeRGy164/LivingDocumentation 14 | Copyright Michaël Hompus 2019 15 | https://github.com/eNeRGy164/LivingDocumentation 16 | git 17 | Living Documentation; LivingDocumentation; Roslyn; UML; Generation; 18 | en-US 19 | 0.1.0 20 | MIT 21 | 22 | true 23 | true 24 | true 25 | true 26 | snupkg 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Statements/LivingDocumentation.Statements.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | LivingDocumentation 7 | 8 | latest 9 | true 10 | enable 11 | 12 | Michaël Hompus 13 | https://github.com/eNeRGy164/LivingDocumentation 14 | Copyright Michaël Hompus 2019 15 | https://github.com/eNeRGy164/LivingDocumentation 16 | git 17 | Living Documentation; LivingDocumentation; Roslyn; UML; Generation; 18 | en-US 19 | 0.1.0 20 | MIT 21 | 22 | true 23 | true 24 | true 25 | true 26 | snupkg 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Descriptions.Tests/MemberDescriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Description.Tests; 2 | 3 | [TestClass] 4 | public class MemberDescriptionTests 5 | { 6 | [TestMethod] 7 | public void MembersWithSameTypeAndNameShouldBeEqual() 8 | { 9 | // Act 10 | var descriptionX = new PropertyDescription("Type", "Name"); 11 | var descriptionY = new PropertyDescription("Type", "Name"); 12 | 13 | // Assert 14 | descriptionX.Should().Be(descriptionY); 15 | descriptionX.GetHashCode().Should().Be(descriptionY.GetHashCode()); 16 | } 17 | 18 | [TestMethod] 19 | public void MembersWithDifferentTypeShouldNotBeEqual() 20 | { 21 | // Act 22 | var descriptionX = new PropertyDescription("Type", "Name"); 23 | var descriptionY = new FieldDescription("Type", "Name"); 24 | 25 | // Assert 26 | descriptionX.Should().NotBe(descriptionY); 27 | descriptionX.GetHashCode().Should().NotBe(descriptionY.GetHashCode()); 28 | } 29 | 30 | [TestMethod] 31 | public void MembersWithDifferentNamesShouldNotBeEqual() 32 | { 33 | // Act 34 | var descriptionX = new PropertyDescription("Type", "NameA"); 35 | var descriptionY = new PropertyDescription("Type", "NameB"); 36 | 37 | // Assert 38 | descriptionX.Should().NotBe(descriptionY); 39 | descriptionX.GetHashCode().Should().NotBe(descriptionY.GetHashCode()); 40 | } 41 | 42 | [TestMethod] 43 | public void MembersWithDifferentTypesAndNamesShouldNotBeEqual() 44 | { 45 | // Act 46 | var descriptionX = new PropertyDescription("Type", "NameA"); 47 | var descriptionY = new FieldDescription("Type", "NameB"); 48 | 49 | // Assert 50 | descriptionX.Should().NotBe(descriptionY); 51 | descriptionX.GetHashCode().Should().NotBe(descriptionY.GetHashCode()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/LivingDocumentation.UML/LivingDocumentation.UML.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | LivingDocumentation.Uml 7 | 8 | latest 9 | true 10 | enable 11 | 12 | Michaël Hompus 13 | https://github.com/eNeRGy164/LivingDocumentation 14 | Copyright Michaël Hompus 2019 15 | https://github.com/eNeRGy164/LivingDocumentation 16 | git 17 | Living Documentation; LivingDocumentation; Roslyn; UML; Generation; 18 | en-US 19 | 0.1.0 20 | MIT 21 | 22 | true 23 | true 24 | true 25 | true 26 | snupkg 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.RenderExtensions.Tests/IEnumerableStringExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.RenderExtensions.Tests; 2 | 3 | [TestClass] 4 | public class IEnumerableStringExtensionsTests 5 | { 6 | [TestMethod] 7 | public void ExtensionMethodShouldGuardAgainstNRE() 8 | { 9 | // Assign 10 | IEnumerable list = default; 11 | 12 | // Act 13 | Action act = () => list.StartsWith(""); 14 | 15 | // Assert 16 | act.Should().ThrowExactly() 17 | .WithParameterName("list"); 18 | } 19 | 20 | [TestMethod] 21 | public void ExtensionMethodShouldGuardAgainstParameterNRE() 22 | { 23 | // Assign 24 | var list = new List(); 25 | 26 | // Act 27 | Action act = () => list.StartsWith(null); 28 | 29 | // Assert 30 | act.Should().ThrowExactly() 31 | .WithParameterName("partialName"); 32 | } 33 | 34 | [DataRow("", 4, DisplayName = "An empty string will match against every entry")] 35 | [DataRow("System.Object", 1, DisplayName = "An exact match")] 36 | [DataRow("LivingDocumentation", 2, DisplayName = "An item matching multiple entries")] 37 | [DataRow("LivingDocumentation.RenderExtensions.Tests.Class<", 1, DisplayName = "Match against an entry with a generic pattern")] 38 | [TestMethod] 39 | public void ExpectTheFilterToBeAppliedCorrectlyOnTheList(string value, int expectation) 40 | { 41 | // Assign 42 | var list = new List() 43 | { 44 | "", 45 | "System.Object", 46 | "LivingDocumentation.RenderExtensions.Tests.Class", 47 | "LivingDocumentation.RenderExtensions.Tests.Class" 48 | }; 49 | 50 | // Act 51 | var result = list.StartsWith(value); 52 | 53 | // Assert 54 | result.Should().HaveCount(expectation); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/LivingDocumentation.Descriptions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | LivingDocumentation 7 | 8 | latest 9 | true 10 | enable 11 | 12 | Michaël Hompus 13 | https://github.com/eNeRGy164/LivingDocumentation 14 | Copyright Michaël Hompus 2019 15 | https://github.com/eNeRGy164/LivingDocumentation 16 | git 17 | Living Documentation; LivingDocumentation; Roslyn; UML; Generation; 18 | en-US 19 | 0.1.0 20 | MIT 21 | 22 | true 23 | true 24 | true 25 | true 26 | snupkg 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Analyzer/AnalyzerSetup.cs: -------------------------------------------------------------------------------- 1 | using Buildalyzer; 2 | using Buildalyzer.Workspaces; 3 | 4 | namespace LivingDocumentation; 5 | 6 | public sealed class AnalyzerSetup : IDisposable 7 | { 8 | public IEnumerable Projects; 9 | public readonly Workspace Workspace; 10 | 11 | private AnalyzerSetup(AnalyzerManager Manager) 12 | { 13 | this.Workspace = Manager.GetWorkspace(); 14 | this.Projects = this.Workspace.CurrentSolution.Projects; 15 | } 16 | 17 | public void Dispose() 18 | { 19 | this.Workspace.Dispose(); 20 | } 21 | 22 | public static AnalyzerSetup BuildSolutionAnalyzer(string solutionFile, IEnumerable excludedProjects = default!) 23 | { 24 | var excludedSet = excludedProjects is not null ? new HashSet(excludedProjects, StringComparer.OrdinalIgnoreCase) : new(0); 25 | 26 | var manager = new AnalyzerManager(solutionFile); 27 | var analysis = new AnalyzerSetup(manager); 28 | 29 | var assembliesInSolution = analysis.Workspace.CurrentSolution.Projects.Select(p => p.AssemblyName).ToList(); 30 | 31 | // Every project in the solution, except unit test projects 32 | analysis.Projects = analysis.Projects 33 | .Where(p => !ProjectContainsTestPackageReference(manager, p)) 34 | .Where(p => string.IsNullOrEmpty(p.FilePath) || !excludedSet.Contains(p.FilePath)); 35 | 36 | return analysis; 37 | } 38 | 39 | public static AnalyzerSetup BuildProjectAnalyzer(string projectFile) 40 | { 41 | var manager = new AnalyzerManager(); 42 | manager.GetProject(projectFile); 43 | 44 | return new AnalyzerSetup(manager); 45 | } 46 | 47 | private static bool ProjectContainsTestPackageReference(AnalyzerManager manager, Project p) 48 | { 49 | return manager.Projects.First(mp => p.Id.Id == mp.Value.ProjectGuid).Value.ProjectFile.PackageReferences.Any(pr => pr.Name.Contains("Test", StringComparison.Ordinal)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/AnalyzerSetupVerification/SolutionWithoutTests.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33213.308 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Project", "Project\Project.csproj", "{5CA13AC2-756B-45BB-9EC8-9F71C31A8F35}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtherProject", "OtherProject\OtherProject.csproj", "{50FB5E30-AB8D-4D9F-BCDE-98F0BACC084F}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnotherProject", "AnotherProject\AnotherProject.csproj", "{6412962A-D13A-4B91-B2A0-6F592BCB5CAF}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {5CA13AC2-756B-45BB-9EC8-9F71C31A8F35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {5CA13AC2-756B-45BB-9EC8-9F71C31A8F35}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {5CA13AC2-756B-45BB-9EC8-9F71C31A8F35}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {5CA13AC2-756B-45BB-9EC8-9F71C31A8F35}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {50FB5E30-AB8D-4D9F-BCDE-98F0BACC084F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {50FB5E30-AB8D-4D9F-BCDE-98F0BACC084F}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {50FB5E30-AB8D-4D9F-BCDE-98F0BACC084F}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {50FB5E30-AB8D-4D9F-BCDE-98F0BACC084F}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {6412962A-D13A-4B91-B2A0-6F592BCB5CAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {6412962A-D13A-4B91-B2A0-6F592BCB5CAF}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {6412962A-D13A-4B91-B2A0-6F592BCB5CAF}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {6412962A-D13A-4B91-B2A0-6F592BCB5CAF}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {FF21EC90-3752-4496-B630-3FD0CEB652E1} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /src/LivingDocumentation.UML/Fragments/InteractionFragmentExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Uml; 2 | 3 | public static class InteractionFragmentExtensions 4 | { 5 | /// 6 | /// Query all descendants from this level down. 7 | /// 8 | /// The type of fragment to filter on. 9 | /// Returns a readonly list of child fragments. 10 | public static IReadOnlyList Descendants(this IEnumerable nodes) 11 | where TFragment : InteractionFragment 12 | { 13 | var result = new List(); 14 | 15 | foreach (var node in nodes) 16 | { 17 | switch (node) 18 | { 19 | case TFragment t: 20 | result.Add(t); 21 | break; 22 | 23 | case Alt a: 24 | result.AddRange(a.Sections.SelectMany(s => s.Fragments).Descendants()); 25 | break; 26 | 27 | default: 28 | break; 29 | } 30 | } 31 | 32 | return result; 33 | } 34 | 35 | /// 36 | /// Query all parent fragments from this fragment up. 37 | /// 38 | /// Returns a list of parent fragments. 39 | public static IReadOnlyList Ancestors(this InteractionFragment fragment) 40 | { 41 | var result = new List(); 42 | 43 | var parent = fragment.Parent; 44 | while (parent != null) 45 | { 46 | result.Add(parent); 47 | 48 | parent = parent.Parent; 49 | } 50 | 51 | return result; 52 | } 53 | 54 | /// 55 | /// Query all sibling before the current fragment. 56 | /// 57 | /// Returns a readonly list of siblings comming before this fragment. 58 | public static IReadOnlyList StatementsBeforeSelf(this InteractionFragment fragment) 59 | { 60 | if (fragment.Parent != null) 61 | { 62 | return fragment.Parent.Fragments.TakeWhile(s => s != fragment).ToList(); 63 | } 64 | 65 | return new List(0); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Abstractions/LivingDocumentation.Abstractions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | LivingDocumentation 7 | 8 | latest 9 | true 10 | enable 11 | 12 | Michaël Hompus 13 | https://github.com/eNeRGy164/LivingDocumentation 14 | Copyright Michaël Hompus 2019 15 | https://github.com/eNeRGy164/LivingDocumentation 16 | git 17 | Living Documentation; LivingDocumentation; Roslyn; UML; Generation; 18 | en-US 19 | 0.1.0 20 | MIT 21 | 22 | true 23 | true 24 | true 25 | true 26 | snupkg 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | <_Parameter1>LivingDocumentation.Analyzer 44 | 45 | 46 | <_Parameter1>LivingDocumentation.Descriptions 47 | 48 | 49 | <_Parameter1>LivingDocumentation.Statements 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.RenderExtensions.Tests/InvocationDescriptionExtensionsTests.MatchesMethod.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.RenderExtensions.Tests; 2 | 3 | public partial class InvocationDescriptionExtensionsTests 4 | { 5 | [TestMethod] 6 | public void MatchesMethod_NullMethod_Should_Throw() 7 | { 8 | // Assign 9 | var invocation = new InvocationDescription("System.Object", "Method"); 10 | 11 | // Act 12 | Action action = () => invocation.MatchesMethod(default); 13 | 14 | // Assert 15 | action.Should().Throw() 16 | .And.ParamName.Should().Be("method"); 17 | } 18 | 19 | [TestMethod] 20 | public void MatchesMethod_NullInvocation_Should_Throw() 21 | { 22 | // Assign 23 | var method = new MethodDescription("void", "Method"); 24 | 25 | // Act 26 | Action action = () => ((InvocationDescription)default).MatchesMethod(method); 27 | 28 | // Assert 29 | action.Should().Throw() 30 | .And.ParamName.Should().Be("invocation"); 31 | } 32 | 33 | [TestMethod] 34 | public void MatchesMethod_InvocationWithoutArguments_And_MethodWithoutParameters_Should_Match() 35 | { 36 | // Assign 37 | var method = new MethodDescription("void", "Method"); 38 | 39 | var invocation = new InvocationDescription("System.Object", "Method"); 40 | 41 | // Act 42 | var result = invocation.MatchesMethod(method); 43 | 44 | // Assert 45 | result.Should().BeTrue(); 46 | } 47 | 48 | [TestMethod] 49 | public void MatchesMethod_InvocationNameEqualsMethodName_Should_Match() 50 | { 51 | // Assign 52 | var method = new MethodDescription("void", "Method"); 53 | 54 | var invocation = new InvocationDescription("System.Object", "Method"); 55 | 56 | // Act 57 | var result = invocation.MatchesMethod(method); 58 | 59 | // Assert 60 | result.Should().BeTrue(); 61 | } 62 | 63 | [TestMethod] 64 | public void MatchesMethod_InvocationNameNotEqualsMethodName_Should_NotMatch() 65 | { 66 | // Assign 67 | var method = new MethodDescription("void", "method"); 68 | 69 | var invocation = new InvocationDescription("System.Object", "Method"); 70 | 71 | // Act 72 | var result = invocation.MatchesMethod(method); 73 | 74 | // Assert 75 | result.Should().BeFalse(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Build, Test and Package 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | env: 14 | DOTNET_NOLOGO: true 15 | 16 | steps: 17 | - name: Fetch all history for all tags and branches 18 | uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Install GitVersion 23 | uses: gittools/actions/gitversion/setup@v0.9.15 24 | with: 25 | versionSpec: 5.x 26 | 27 | - name: Use GitVersion 28 | id: gitversion 29 | uses: gittools/actions/gitversion/execute@v0.9.15 30 | 31 | - name: Setup .NET 7.0.x 32 | uses: actions/setup-dotnet@v3 33 | with: 34 | dotnet-version: | 35 | 7.0.x 36 | 37 | - name: Restore dependencies 38 | run: dotnet restore 39 | 40 | - name: Build 41 | run: dotnet build --configuration Release --no-restore 42 | 43 | - name: Test 44 | run: dotnet test --configuration Release --no-restore --no-build --nologo --verbosity minimal --settings LivingDocumentation.runsettings --collect:"XPlat Code Coverage" --results-directory ${{ github.workspace }}/coverage 45 | 46 | - name: ReportGenerator to merge coverage files 47 | uses: danielpalme/ReportGenerator-GitHub-Action@5.1.13 48 | with: 49 | reports: coverage/**/*.xml 50 | targetdir: ${{ github.workspace }} 51 | reporttypes: lcov;Cobertura 52 | verbosity: Info 53 | 54 | - name: Code Coverage Summary Report 55 | uses: irongut/CodeCoverageSummary@v1.3.0 56 | with: 57 | filename: Cobertura.xml 58 | badge: true 59 | format: markdown 60 | output: both 61 | 62 | - name: Add Coverage PR Comment 63 | uses: marocchino/sticky-pull-request-comment@v2 64 | if: github.event_name == 'pull_request' 65 | with: 66 | recreate: true 67 | path: code-coverage-results.md 68 | 69 | - name: coveralls 70 | uses: coverallsapp/github-action@1.1.3 71 | with: 72 | github-token: ${{ secrets.GITHUB_TOKEN }} 73 | path-to-lcov: lcov.info 74 | 75 | - name: Pack 76 | run: dotnet pack --configuration Release --no-restore --no-build --verbosity minimal -property:PackageVersion=${{ steps.gitversion.outputs.NuGetVersion }} --output ${{ github.workspace }}/packages 77 | 78 | - name: Upload Artifact 79 | uses: actions/upload-artifact@v3 80 | with: 81 | name: nupkg 82 | path: packages/* 83 | -------------------------------------------------------------------------------- /tests/AnalyzerSetupVerification/SolutionWithTests.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33213.308 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Project", "Project\Project.csproj", "{3394B9C3-A5D3-4145-B70D-3D54B8852596}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtherProject", "OtherProject\OtherProject.csproj", "{586E3262-BB21-4F37-BA2D-3B56706D4746}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject", "TestProject\TestProject.csproj", "{E9E8CDAD-2B87-4C53-A1E0-7BC625375ADA}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnotherProject", "AnotherProject\AnotherProject.csproj", "{91EA6563-C625-43F4-9C37-23B7DB54FD73}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {3394B9C3-A5D3-4145-B70D-3D54B8852596}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {3394B9C3-A5D3-4145-B70D-3D54B8852596}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {3394B9C3-A5D3-4145-B70D-3D54B8852596}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {3394B9C3-A5D3-4145-B70D-3D54B8852596}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {586E3262-BB21-4F37-BA2D-3B56706D4746}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {586E3262-BB21-4F37-BA2D-3B56706D4746}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {586E3262-BB21-4F37-BA2D-3B56706D4746}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {586E3262-BB21-4F37-BA2D-3B56706D4746}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {E9E8CDAD-2B87-4C53-A1E0-7BC625375ADA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {E9E8CDAD-2B87-4C53-A1E0-7BC625375ADA}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {E9E8CDAD-2B87-4C53-A1E0-7BC625375ADA}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {E9E8CDAD-2B87-4C53-A1E0-7BC625375ADA}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {91EA6563-C625-43F4-9C37-23B7DB54FD73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {91EA6563-C625-43F4-9C37-23B7DB54FD73}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {91EA6563-C625-43F4-9C37-23B7DB54FD73}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {91EA6563-C625-43F4-9C37-23B7DB54FD73}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {BD82645A-5556-4DFF-8F8A-AEF5EAEE04A9} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Analyzer/LivingDocumentation.Analyzer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | LivingDocumentation.Analyzer 8 | LivingDocumentation.Analyzer 9 | 10 | latest 11 | true 12 | enable 13 | 14 | true 15 | livingdoc-analyze 16 | 17 | Michaël Hompus 18 | https://github.com/eNeRGy164/LivingDocumentation 19 | Tool to analyze a solution and output the detected code structure to enable rendering. 20 | Copyright Michaël Hompus 2019 21 | https://github.com/eNeRGy164/LivingDocumentation 22 | git 23 | Living Documentation; LivingDocumentation; Roslyn; UML; Generation; 24 | en-US 25 | 0.1.0 26 | MIT 27 | 28 | true 29 | true 30 | true 31 | true 32 | snupkg 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Analyzer/Analyzers/BranchingAnalyzer.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | internal class BranchingAnalyzer(SemanticModel semanticModel, List statements) : CSharpSyntaxWalker 4 | { 5 | public override void VisitIfStatement(IfStatementSyntax node) 6 | { 7 | var ifStatement = new If(); 8 | statements.Add(ifStatement); 9 | 10 | var ifSection = new IfElseSection(); 11 | ifStatement.Sections.Add(ifSection); 12 | 13 | ifSection.Condition = node.Condition.ToString(); 14 | 15 | var ifInvocationAnalyzer = new InvocationsAnalyzer(semanticModel, ifSection.Statements); 16 | ifInvocationAnalyzer.Visit(node.Statement); 17 | 18 | var elseNode = node.Else; 19 | while (elseNode != null) 20 | { 21 | var section = new IfElseSection(); 22 | ifStatement.Sections.Add(section); 23 | 24 | var elseInvocationAnalyzer = new InvocationsAnalyzer(semanticModel, section.Statements); 25 | elseInvocationAnalyzer.Visit(elseNode.Statement); 26 | 27 | if (elseNode.Statement.IsKind(SyntaxKind.IfStatement)) 28 | { 29 | var elseIfNode = (IfStatementSyntax)elseNode.Statement; 30 | section.Condition = elseIfNode.Condition.ToString(); 31 | 32 | elseNode = elseIfNode.Else; 33 | } 34 | else 35 | { 36 | elseNode = null; 37 | } 38 | } 39 | } 40 | 41 | public override void VisitSwitchStatement(SwitchStatementSyntax node) 42 | { 43 | var switchStatement = new Switch(); 44 | statements.Add(switchStatement); 45 | 46 | switchStatement.Expression = node.Expression.ToString(); 47 | 48 | foreach (var section in node.Sections) 49 | { 50 | var switchSection = new SwitchSection(); 51 | switchStatement.Sections.Add(switchSection); 52 | 53 | switchSection.Labels.AddRange(section.Labels.Select(l => Label(l))); 54 | 55 | var invocationAnalyzer = new InvocationsAnalyzer(semanticModel, switchSection.Statements); 56 | invocationAnalyzer.Visit(section); 57 | } 58 | } 59 | 60 | private static string Label(SwitchLabelSyntax label) 61 | { 62 | return label switch 63 | { 64 | CasePatternSwitchLabelSyntax casePattern when casePattern.WhenClause?.Condition is not null => $"{casePattern.Pattern} when {casePattern.WhenClause.Condition}", 65 | CasePatternSwitchLabelSyntax casePattern => casePattern.Pattern.ToString(), 66 | CaseSwitchLabelSyntax @case => @case.Value.ToString(), 67 | DefaultSwitchLabelSyntax @default => @default.Keyword.ToString(), 68 | _ => throw new ArgumentOutOfRangeException(nameof(label)), 69 | }; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Extensions/IHaveModifiersExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public static class IHaveModifiersExtensions 4 | { 5 | public static bool IsStatic(this IHaveModifiers iHaveModifiers) 6 | { 7 | return (iHaveModifiers.Modifiers & Modifier.Static) == Modifier.Static; 8 | } 9 | 10 | public static bool IsPublic(this IHaveModifiers iHaveModifiers) 11 | { 12 | return (iHaveModifiers.Modifiers & Modifier.Public) == Modifier.Public; 13 | } 14 | 15 | public static bool IsInternal(this IHaveModifiers iHaveModifiers) 16 | { 17 | return (iHaveModifiers.Modifiers & Modifier.Internal) == Modifier.Internal; 18 | } 19 | 20 | public static bool IsProtected(this IHaveModifiers iHaveModifiers) 21 | { 22 | return (iHaveModifiers.Modifiers & Modifier.Protected) == Modifier.Protected; 23 | } 24 | 25 | public static bool IsAbstract(this IHaveModifiers iHaveModifiers) 26 | { 27 | return (iHaveModifiers.Modifiers & Modifier.Abstract) == Modifier.Abstract; 28 | } 29 | 30 | public static bool IsPrivate(this IHaveModifiers iHaveModifiers) 31 | { 32 | return (iHaveModifiers.Modifiers & Modifier.Private) == Modifier.Private; 33 | } 34 | 35 | public static bool IsAsync(this IHaveModifiers iHaveModifiers) 36 | { 37 | return (iHaveModifiers.Modifiers & Modifier.Async) == Modifier.Async; 38 | } 39 | 40 | public static bool IsOverride(this IHaveModifiers iHaveModifiers) 41 | { 42 | return (iHaveModifiers.Modifiers & Modifier.Override) == Modifier.Override; 43 | } 44 | 45 | public static bool IsReadonly(this IHaveModifiers iHaveModifiers) 46 | { 47 | return (iHaveModifiers.Modifiers & Modifier.Readonly) == Modifier.Readonly; 48 | } 49 | 50 | public static bool IsConst(this IHaveModifiers iHaveModifiers) 51 | { 52 | return (iHaveModifiers.Modifiers & Modifier.Const) == Modifier.Const; 53 | } 54 | 55 | public static bool IsPartial(this IHaveModifiers iHaveModifiers) 56 | { 57 | return (iHaveModifiers.Modifiers & Modifier.Partial) == Modifier.Partial; 58 | } 59 | 60 | public static bool IsExtern(this IHaveModifiers iHaveModifiers) 61 | { 62 | return (iHaveModifiers.Modifiers & Modifier.Extern) == Modifier.Extern; 63 | } 64 | 65 | public static bool IsNew(this IHaveModifiers iHaveModifiers) 66 | { 67 | return (iHaveModifiers.Modifiers & Modifier.New) == Modifier.New; 68 | } 69 | 70 | public static bool IsSealed(this IHaveModifiers iHaveModifiers) 71 | { 72 | return (iHaveModifiers.Modifiers & Modifier.Sealed) == Modifier.Sealed; 73 | } 74 | 75 | public static bool IsUnsafe(this IHaveModifiers iHaveModifiers) 76 | { 77 | return (iHaveModifiers.Modifiers & Modifier.Unsafe) == Modifier.Unsafe; 78 | } 79 | 80 | public static bool IsVirtual(this IHaveModifiers iHaveModifiers) 81 | { 82 | return (iHaveModifiers.Modifiers & Modifier.Virtual) == Modifier.Virtual; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.RenderExtensions.Tests/TypeDescriptionListExtensionsTests.PopulateInheritedMembers.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.RenderExtensions.Tests; 2 | 3 | public partial class TypeDescriptionListExtensions 4 | { 5 | [TestMethod] 6 | public void PopulateInheritedMembers_NullTypes_Should_Throw() 7 | { 8 | // Assign 9 | var types = (List)default; 10 | 11 | // Act 12 | Action action = () => types.PopulateInheritedMembers(); 13 | 14 | // Assert 15 | action.Should().Throw() 16 | .And.ParamName.Should().Be("types"); 17 | } 18 | 19 | [TestMethod] 20 | public void PopulateInheritedMembers_NoBaseTypes_Should_NotThrow() 21 | { 22 | // Assign 23 | var types = new[] { 24 | new TypeDescription(TypeType.Class, "Test") 25 | }; 26 | 27 | // Act 28 | Action action = () => types.PopulateInheritedMembers(); 29 | 30 | // Assert 31 | action.Should().NotThrow(); 32 | } 33 | 34 | [TestMethod] 35 | public void PopulateInheritedMembers_UnknownBaseTypes_Should_NotThrow() 36 | { 37 | // Assign 38 | var types = new[] { 39 | new TypeDescription(TypeType.Class, "Test") 40 | { 41 | BaseTypes = 42 | { 43 | "XXX" 44 | } 45 | }, 46 | }; 47 | 48 | // Act 49 | Action action = () => types.PopulateInheritedMembers(); 50 | 51 | // Assert 52 | action.Should().NotThrow(); 53 | } 54 | 55 | [TestMethod] 56 | public void PopulateInheritedMembers_BaseTypeMembers_Should_BeCopiedToImplementingType() 57 | { 58 | // Assign 59 | var baseType = new TypeDescription(TypeType.Class, "BaseTest"); 60 | baseType.AddMember(new FieldDescription("int", "number")); 61 | 62 | var types = new[] { 63 | new TypeDescription(TypeType.Class, "Test") 64 | { 65 | BaseTypes = 66 | { 67 | "BaseTest" 68 | } 69 | }, 70 | baseType 71 | }; 72 | 73 | // Act 74 | types.PopulateInheritedMembers(); 75 | 76 | // Assert 77 | types[0].Fields.Should().HaveCount(1); 78 | types[0].Fields[0].Should().NotBeNull(); 79 | types[0].Fields[0].Name.Should().Be("number"); 80 | } 81 | 82 | [TestMethod] 83 | public void PopulateInheritedMembers_PrivateFields_Should_NotBeCopiedToImplementingType() 84 | { 85 | // Assign 86 | var baseType = new TypeDescription(TypeType.Class, "BaseTest"); 87 | baseType.AddMember(new FieldDescription("int", "number")); 88 | baseType.AddMember(new FieldDescription("int", "number2") { Modifiers = Modifier.Private }); 89 | 90 | var types = new[] { 91 | new TypeDescription(TypeType.Class, "Test") 92 | { 93 | BaseTypes = 94 | { 95 | "BaseTest" 96 | } 97 | }, 98 | baseType 99 | }; 100 | 101 | // Act 102 | types.PopulateInheritedMembers(); 103 | 104 | // Assert 105 | types[0].Fields.Should().HaveCount(1); 106 | types[0].Fields[0].Name.Should().Be("number"); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Analyzer.Tests/Analyzers/BranchingAnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Analyzer.Tests.Analyzers; 2 | 3 | [TestClass] 4 | public class BranchingAnalyzerTests 5 | { 6 | [DynamicData(nameof(GetIfStatements), DynamicDataSourceType.Method)] 7 | [TestMethod] 8 | public void ShouldParseIfBlocksCorrectly(string code, int sections, string[] conditions) 9 | { 10 | // Assign 11 | var source = @$" 12 | public class Test 13 | {{ 14 | public int Something; 15 | void Method() 16 | {{ 17 | {code} 18 | }} 19 | }} 20 | "; 21 | 22 | // Act 23 | var types = TestHelper.VisitSyntaxTree(source); 24 | 25 | // Assert 26 | types[0].Methods[0].Statements[0].Should().BeOfType(); 27 | 28 | var @if = (If)types[0].Methods[0].Statements[0]; 29 | @if.Sections.Should().HaveCount(sections); 30 | @if.Sections.Select(s => s.Condition).Should().Equal(conditions); 31 | } 32 | 33 | private static IEnumerable GetIfStatements() 34 | { 35 | yield return new object[] { "if (Something == 1) { }", 1, new[] { "Something == 1" } }; 36 | yield return new object[] { "if (Something == 1) { } else { }", 2, new[] { "Something == 1", null } }; 37 | yield return new object[] { "if (Something == 1) { } else if (Something == 2) { }", 2, new[] { "Something == 1", "Something == 2" } }; 38 | yield return new object[] { "if (Something == 1) { } else if (Something == 2) { } else { }", 3, new[] { "Something == 1", "Something == 2", null } }; 39 | } 40 | 41 | [DynamicData(nameof(GetSwitchStatements), DynamicDataSourceType.Method)] 42 | [TestMethod] 43 | public void ShouldParseSwitchBlocksCorrectly(string code, int sections, string[][] conditions) 44 | { 45 | // Assign 46 | var source = @$" 47 | public class Test 48 | {{ 49 | public int Something; 50 | void Method() 51 | {{ 52 | {code} 53 | }} 54 | }} 55 | "; 56 | 57 | // Act 58 | var types = TestHelper.VisitSyntaxTree(source, "CS1522"); 59 | 60 | // Assert 61 | types[0].Methods[0].Statements[0].Should().BeOfType(); 62 | 63 | var @switch = (Switch)types[0].Methods[0].Statements[0]; 64 | @switch.Sections.Should().HaveCount(sections); 65 | 66 | for (var i = 0; i < @switch.Sections.Count; i++) 67 | { 68 | @switch.Sections[i].Labels.Should().Equal(conditions[i]); 69 | } 70 | } 71 | 72 | private static IEnumerable GetSwitchStatements() 73 | { 74 | yield return new object[] { "switch (Something) { }", 0, new[] { Array.Empty() } }; 75 | yield return new object[] { "switch (Something) { case 1: break; }", 1, new[] { new[] { "1" } } }; 76 | yield return new object[] { "switch (Something) { case 1: case 2: break; }", 1, new[] { new[] { "1", "2" } } }; 77 | yield return new object[] { "switch (Something) { case 1: break; case 2: break; }", 2, new[] { new[] { "1" }, new[] { "2" } } }; 78 | yield return new object[] { "switch (Something) { default: break; }", 1, new[] { new[] { "default" } } }; 79 | yield return new object[] { "switch (Something) { case 1 when true: break; }", 1, new[] { new[] { "1 when true" } } }; 80 | yield return new object[] { "switch (Something) { case int value: break; }", 1, new[] { new[] { "int value" } } }; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Analyzer/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Newtonsoft.Json; 3 | 4 | namespace LivingDocumentation; 5 | 6 | public static partial class Program 7 | { 8 | private static ParserResult? ParsedResults; 9 | 10 | public static Options RuntimeOptions { get; private set; } = new Options(); 11 | 12 | public static async Task Main(string[] args) 13 | { 14 | ParsedResults = Parser.Default.ParseArguments(args); 15 | 16 | await ParsedResults.MapResult( 17 | options => RunApplicationAsync(options), 18 | errors => Task.FromResult(1) 19 | ).ConfigureAwait(false); 20 | } 21 | 22 | private static async Task RunApplicationAsync(Options options) 23 | { 24 | RuntimeOptions = options; 25 | 26 | var types = new List(); 27 | 28 | var stopwatch = Stopwatch.StartNew(); 29 | 30 | using (var analyzer = options.SolutionPath is not null 31 | ? AnalyzerSetup.BuildSolutionAnalyzer(options.SolutionPath, options.ExcludedProjectPaths) 32 | : AnalyzerSetup.BuildProjectAnalyzer(options.ProjectPath!)) 33 | { 34 | await AnalyzeWorkspace(types, analyzer).ConfigureAwait(false); 35 | } 36 | 37 | stopwatch.Stop(); 38 | 39 | // Write analysis 40 | var serializerSettings = JsonDefaults.SerializerSettings(); 41 | serializerSettings.Formatting = options.PrettyPrint ? Formatting.Indented : Formatting.None; 42 | 43 | var result = JsonConvert.SerializeObject(types.OrderBy(t => t.FullName), serializerSettings); 44 | 45 | await File.WriteAllTextAsync(options.OutputPath!, result).ConfigureAwait(false); 46 | 47 | if (!options.Quiet) 48 | { 49 | Console.WriteLine($"Living Documentation Analysis output generated in {stopwatch.ElapsedMilliseconds}ms at {options.OutputPath}"); 50 | Console.WriteLine($"{types.Count} types found"); 51 | } 52 | } 53 | 54 | private static async Task AnalyzeWorkspace(List types, AnalyzerSetup analysis) 55 | { 56 | foreach (var project in analysis.Projects) 57 | { 58 | await AnalyzeProjectAsyc(types, project).ConfigureAwait(false); 59 | } 60 | } 61 | 62 | private static async Task AnalyzeProjectAsyc(List types, Project project) 63 | { 64 | var compilation = await project.GetCompilationAsync().ConfigureAwait(false); 65 | if (compilation is null) 66 | { 67 | return; 68 | } 69 | 70 | if (RuntimeOptions.VerboseOutput) 71 | { 72 | var diagnostics = compilation.GetDiagnostics(); 73 | if (diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error)) 74 | { 75 | Console.WriteLine($"The following errors occured during compilation of project '{project.FilePath}'"); 76 | foreach (var diagnostic in diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)) 77 | { 78 | Console.WriteLine("- " + diagnostic.ToString()); 79 | } 80 | } 81 | } 82 | 83 | // Every file in the project 84 | foreach (var syntaxTree in compilation.SyntaxTrees) 85 | { 86 | var semanticModel = compilation.GetSemanticModel(syntaxTree, true); 87 | 88 | var visitor = new SourceAnalyzer(semanticModel, types); 89 | visitor.Visit(syntaxTree.GetRoot()); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.RenderExtensions.Tests/IEnumerableIAttributeDescriptionExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.RenderExtensions.Tests; 2 | 3 | [TestClass] 4 | public class IEnumerableIAttributeDescriptionExtensionsTests 5 | { 6 | [DataRow("OfType", DisplayName = "OfType(string) should guard against a null reference exception")] 7 | [DataRow("HasAttribute", DisplayName = "HasAttribute(string) should guard against a null reference exception")] 8 | [TestMethod] 9 | public void ExtensionMethodShouldGuardAgainstNRE(string methodName) 10 | { 11 | // Assign 12 | IEnumerable list = default; 13 | 14 | var method = typeof(IEnumerableIAttributeDescriptionExtensions).GetMethod(methodName); 15 | var parameters = new object[] { list, "" }.ToArray(); 16 | 17 | // Act 18 | Action action = () => method.Invoke(null, parameters); 19 | 20 | // Assert 21 | action.Should().ThrowExactly() 22 | .WithInnerExceptionExactly() 23 | .And.ParamName.Should().Be("list"); 24 | } 25 | 26 | [DataRow("Attribute1Attribute", 1, DisplayName = "When the attribute exists in the list, return the exact match")] 27 | [DataRow("Attribute3Attribute", 2, DisplayName = "When the attribute exists multiple times in the list, return all matches")] 28 | [DataRow("Attribute4Attribute", 0, DisplayName = "When the attribute does not exists in the list, return no matches")] 29 | [DataRow("attribute1attribute", 0, DisplayName = "When the attribute exists with a different casing in the list, return no matches")] 30 | [TestMethod] 31 | public void ExpectTheFilterToBeAppliedCorrectlyOnTheList(string value, int expectation) 32 | { 33 | // Assign 34 | var list = new List() 35 | { 36 | new AttributeDescription("Attribute1Attribute", "Attribute1"), 37 | new AttributeDescription("Attribute2Attribute", "Attribute2"), 38 | new AttributeDescription("Attribute3Attribute", "Attribute3"), 39 | new AttributeDescription("Attribute3Attribute", "Attribute3") 40 | }; 41 | 42 | // Act 43 | var result = list.OfType(value); 44 | 45 | // Assert 46 | result.Should().HaveCount(expectation); 47 | } 48 | 49 | [DataRow("Attribute1Attribute", true, DisplayName = "When the attribute exists in the list, return `true`")] 50 | [DataRow("Attribute3Attribute", true, DisplayName = "When the attribute exists multiple times in the list, return `true`")] 51 | [DataRow("Attribute4Attribute", false, DisplayName = "When the attribute does not exists in the list, return `false`")] 52 | [DataRow("attribute1attribute", false, DisplayName = "When the attribute exists with a different casing in the list, return `false`")] 53 | [TestMethod] 54 | public void ReturnsTheRightValueIndicatingWhetherThereIsAnAttibuteWithTheTypeInTheList(string value, bool expectation) 55 | { 56 | // Assign 57 | var list = new List() 58 | { 59 | new AttributeDescription("Attribute1Attribute", "Attribute1"), 60 | new AttributeDescription("Attribute2Attribute", "Attribute2"), 61 | new AttributeDescription("Attribute3Attribute", "Attribute3"), 62 | new AttributeDescription("Attribute3Attribute", "Attribute3") 63 | }; 64 | 65 | // Act 66 | var result = list.HasAttribute(value); 67 | 68 | // Assert 69 | result.Should().Be(expectation); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Analyzer.Tests/StructModifierTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Analyzer.Tests; 2 | 3 | [TestClass] 4 | public class StructModifierTests 5 | { 6 | [DataRow("struct Test {}", Modifier.Internal, DisplayName = "A type description about a struct without a modifier should contain the `internal` modifier")] 7 | [DataRow("public struct Test {}", Modifier.Public, DisplayName = "A type description about a `public` struct should contain the `public` modifier")] 8 | [DataRow("internal struct Test {}", Modifier.Internal, DisplayName = "A type description about an `internal` struct should contain the `internal` modifier")] 9 | [TestMethod] 10 | public void StructsShouldHaveTheCorrectAccessModifiers(string @struct, Modifier modifier) 11 | { 12 | // Assign 13 | var source = @struct; 14 | 15 | // Act 16 | var types = TestHelper.VisitSyntaxTree(source); 17 | 18 | // Assert 19 | types[0].Modifiers.Should().Be(modifier); 20 | } 21 | 22 | [TestMethod] 23 | [DataRow("readonly struct Test {}", Modifier.Readonly, DisplayName = "A type description about a `readonly` class should contain the `readonly` modifier")] 24 | [DataRow("unsafe struct Test {}", Modifier.Unsafe, DisplayName = "A type description about an `unsafe` class should contain the `unsafe` modifier")] 25 | public void StructsShouldHaveTheCorrectModifiers(string @struct, Modifier modifier) 26 | { 27 | // Assign 28 | var source = @struct; 29 | 30 | // Act 31 | var types = TestHelper.VisitSyntaxTree(source); 32 | 33 | // Assert 34 | types[0].Modifiers.Should().HaveFlag(modifier); 35 | } 36 | 37 | [DataRow("struct NestedTest {}", Modifier.Private, DisplayName = "A type description about a nested struct without a modifier should contain the `private` modifier")] 38 | [DataRow("private struct NestedTest {}", Modifier.Private, DisplayName = "A type description about a `private` nested struct should contain the `private` modifier")] 39 | [DataRow("public struct NestedTest {}", Modifier.Public, DisplayName = "A type description about a `public` nested struct should contain the `public` modifier")] 40 | [DataRow("internal struct NestedTest {}", Modifier.Internal, DisplayName = "A type description about an `internal` nested struct should contain the `internal` modifier")] 41 | [TestMethod] 42 | public void NestedStructsShouldHaveTheCorrectAccessModifiers(string @struct, Modifier modifier) 43 | { 44 | // Assign 45 | var source = @$" 46 | struct Test 47 | {{ 48 | {@struct} 49 | }} 50 | "; 51 | 52 | // Act 53 | var types = TestHelper.VisitSyntaxTree(source); 54 | 55 | // Assert 56 | types[1].Modifiers.Should().Be(modifier); 57 | } 58 | 59 | [TestMethod] 60 | [DataRow("partial struct NestedTest {}", Modifier.Partial, DisplayName = "A type description about a `partial` nested struct should contain the `partial` modifier")] 61 | [DataRow("readonly struct NestedTest {}", Modifier.Readonly, DisplayName = "A type description about a `readonly` nested struct should contain the `readonly` modifier")] 62 | [DataRow("unsafe struct NestedTest {}", Modifier.Unsafe, DisplayName = "A type description about an `unsafe` nested class struct contain the `unsafe` modifier")] 63 | public void NestedStructsShouldHaveTheCorrectModifiers(string @struct, Modifier modifier) 64 | { 65 | // Assign 66 | var source = @$" 67 | struct Test 68 | {{ 69 | {@struct} 70 | }} 71 | "; 72 | 73 | // Act 74 | var types = TestHelper.VisitSyntaxTree(source); 75 | 76 | // Assert 77 | types[1].Modifiers.Should().HaveFlag(modifier); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Render/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public static class StringExtensions 4 | { 5 | public static bool IsEnumerable(this string type) 6 | { 7 | if (type is null) throw new ArgumentNullException(nameof(type)); 8 | 9 | if (!type.StartsWith("System.Collections.", StringComparison.Ordinal)) 10 | { 11 | return false; 12 | } 13 | else if (type.StartsWith("System.Collections.Generic.", StringComparison.Ordinal)) 14 | { 15 | return !type.Contains("Enumerator") && !type.Contains("Compar") && !type.Contains("Exception"); 16 | } 17 | else if (type.StartsWith("System.Collections.Concurrent.", StringComparison.Ordinal)) 18 | { 19 | return !type.Contains("Partition"); 20 | } 21 | 22 | return !type.Contains("Enumerator") && !type.Contains("Compar") && !type.Contains("Structural") && !type.Contains("Provider"); 23 | } 24 | 25 | public static bool IsGeneric(this string type) 26 | { 27 | if (type is null) throw new ArgumentNullException(nameof(type)); 28 | 29 | return type.IndexOf('>') > -1 && type.TrimEnd().EndsWith(">"); 30 | } 31 | 32 | public static IReadOnlyList GenericTypes(this string type) 33 | { 34 | if (type is null) throw new ArgumentNullException(nameof(type)); 35 | 36 | if (!type.IsGeneric()) 37 | { 38 | return new List(0); 39 | } 40 | 41 | type = type.Trim(); 42 | 43 | var typeParts = type.Substring(type.IndexOf('<') + 1, type.Length - type.IndexOf('<') - 2).Split(','); 44 | var types = new List(); 45 | 46 | foreach (var part in typeParts) 47 | { 48 | if (part.IndexOf('>') > -1 && types.Count > 0 && types.Last().ToCharArray().Count(c => c == '<') > types.Last().ToCharArray().Count(c => c == '>')) 49 | { 50 | types[types.Count - 1] = types[types.Count - 1] + "," + part.Trim(); 51 | } 52 | else 53 | { 54 | types.Add(part.Trim()); 55 | } 56 | } 57 | 58 | return types; 59 | } 60 | 61 | public static string ForDiagram(this string type) 62 | { 63 | if (type is null) throw new ArgumentNullException(nameof(type)); 64 | 65 | if (type.IsGeneric()) 66 | { 67 | var a = type.Substring(0, type.IndexOf('<')).ForDiagram(); 68 | var b = type.GenericTypes().Select(s => s.ForDiagram()); 69 | return $"{a}<{string.Join(", ", b)}>"; 70 | } 71 | else if (type.IndexOf('.') > -1) 72 | { 73 | return type.Substring(type.LastIndexOf('.') + 1); 74 | } 75 | else 76 | { 77 | return type; 78 | } 79 | } 80 | 81 | public static string ToSentenceCase(this string type) 82 | { 83 | if (string.IsNullOrEmpty(type)) 84 | { 85 | return type; 86 | } 87 | 88 | var stringBuilder = new StringBuilder(); 89 | 90 | stringBuilder.Append(char.ToUpper(type[0])); 91 | 92 | for (var i = 1; i < type.Length; i++) 93 | { 94 | if ((char.IsUpper(type[i]) && (!char.IsUpper(type[i - 1]) || ((i + 1 < type.Length) && !char.IsUpper(type[i + 1])))) || (char.IsDigit(type[i]) && !char.IsDigit(type[i - 1]))) 95 | { 96 | stringBuilder.Append(' '); 97 | } 98 | 99 | stringBuilder.Append(type[i]); 100 | } 101 | 102 | return stringBuilder.ToString(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Analyzer.Tests/AnalyzerSetup/AnalyzerSetupTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Analyzer.Tests; 2 | 3 | [TestClass] 4 | public class AnalyzerSetupTests 5 | { 6 | private static readonly string solutionPath = GetSolutionPath(); 7 | 8 | [TestMethod] 9 | public void SolutionShouldLoadAllProjects() 10 | { 11 | // Arrange 12 | var solutionFile = Path.Combine(solutionPath, "SolutionWithoutTests.sln"); 13 | 14 | // Act 15 | using var analyzerSetup = AnalyzerSetup.BuildSolutionAnalyzer(solutionFile); 16 | 17 | // Assert 18 | analyzerSetup.Projects.Should().HaveCount(3); 19 | analyzerSetup.Projects.Should().Satisfy( 20 | p => p.FilePath.EndsWith("Project.csproj"), 21 | p => p.FilePath.EndsWith("OtherProject.csproj"), 22 | p => p.FilePath.EndsWith("AnotherProject.csproj")); 23 | } 24 | 25 | [TestMethod] 26 | public void SolutionShouldFilterTestProjects() 27 | { 28 | // Arrange 29 | var solutionFile = Path.Combine(solutionPath, "SolutionWithTests.sln"); 30 | 31 | // Act 32 | using var analyzerSetup = AnalyzerSetup.BuildSolutionAnalyzer(solutionFile); 33 | 34 | // Assert 35 | analyzerSetup.Projects.Should().HaveCount(3); 36 | analyzerSetup.Projects.Should().Satisfy( 37 | p => p.FilePath.EndsWith("Project.csproj"), 38 | p => p.FilePath.EndsWith("OtherProject.csproj"), 39 | p => p.FilePath.EndsWith("AnotherProject.csproj")); 40 | } 41 | 42 | [TestMethod] 43 | public void SolutionShouldFilterExcludedProject() 44 | { 45 | // Arrange 46 | var solutionFile = Path.Combine(solutionPath, "SolutionWithoutTests.sln"); 47 | var excludeProjectFile = Path.Combine(solutionPath, "OtherProject", "OtherProject.csproj"); 48 | 49 | // Act 50 | using var analyzerSetup = AnalyzerSetup.BuildSolutionAnalyzer(solutionFile, new[] { excludeProjectFile }); 51 | 52 | // Assert 53 | analyzerSetup.Projects.Should().HaveCount(2); 54 | analyzerSetup.Projects.Should().Satisfy( 55 | p => p.FilePath.EndsWith("Project.csproj"), 56 | p => p.FilePath.EndsWith("AnotherProject.csproj")); 57 | } 58 | 59 | [TestMethod] 60 | public void SolutionShouldFilterExcludedProjects() 61 | { 62 | // Arrange 63 | var solutionFile = Path.Combine(solutionPath, "SolutionWithoutTests.sln"); 64 | var excludeProjectFile1 = Path.Combine(solutionPath, "OtherProject", "OtherProject.csproj"); 65 | var excludeProjectFile2 = Path.Combine(solutionPath, "AnotherProject", "AnotherProject.csproj"); 66 | 67 | // Act 68 | using var analyzerSetup = AnalyzerSetup.BuildSolutionAnalyzer(solutionFile, new[] { excludeProjectFile1, excludeProjectFile2 }); 69 | 70 | // Assert 71 | analyzerSetup.Projects.Should().HaveCount(1); 72 | analyzerSetup.Projects.Should().Satisfy(p => p.FilePath.EndsWith("Project.csproj")); 73 | } 74 | 75 | [TestMethod] 76 | public void SolutionShouldLoadProject() 77 | { 78 | // Arrange 79 | var projectFile = Path.Combine(solutionPath, "Project", "Project.csproj"); 80 | 81 | // Act 82 | using var analyzerSetup = AnalyzerSetup.BuildProjectAnalyzer(projectFile); 83 | 84 | // Assert 85 | analyzerSetup.Projects.Should().HaveCount(1); 86 | analyzerSetup.Projects.Should().Satisfy(p => p.FilePath.EndsWith("Project.csproj")); 87 | } 88 | 89 | private static string GetSolutionPath() 90 | { 91 | var currentDirectory = Directory.GetCurrentDirectory().AsSpan(); 92 | 93 | var path = currentDirectory[..(currentDirectory.IndexOf("tests") + 6)]; 94 | 95 | return Path.Combine(path.ToString(), "AnalyzerSetupVerification"); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Extensions.Tests/StringExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Extensions.Tests; 2 | 3 | [TestClass] 4 | public partial class StringExtensionsTests 5 | { 6 | [DataRow(null, "", DisplayName = "A `null` value should return an empty string")] 7 | [DataRow("", "", DisplayName = "An empty string value should return an empty string")] 8 | [DataRow(".", "", DisplayName = "A value with only a dot should return an empty string")] 9 | [DataRow("Class", "Class", DisplayName = "A class name without a namespace should return the class name")] 10 | [DataRow("Namespace.Class", "Class", DisplayName = "A class in a namespace should only return the class name")] 11 | [DataRow(".Class", "Class", DisplayName = "A value starting with a dot should only return the class name")] 12 | [DataRow("Namespace.Class", "Class", DisplayName = "A generic class in a namespace should return the class name with the generic part")] 13 | [TestMethod] 14 | public void ReduceAFullTypeNameToOnlyTheClassPart(string fullname, string expectation) 15 | { 16 | // Act 17 | var result = fullname.ClassName(); 18 | 19 | // Assert 20 | result.Should().Be(expectation); 21 | } 22 | 23 | [DataRow(null, "", DisplayName = "A `null` value should return an empty string")] 24 | [DataRow("", "", DisplayName = "An empty string value should return an empty string")] 25 | [DataRow(".", "", DisplayName = "A value with only a dot should return an empty string")] 26 | [DataRow("Class", "", DisplayName = "A class name without a namespace should return an empty string")] 27 | [DataRow("Namespace.Class", "Namespace", DisplayName = "A class in a namespace should only return the namespace name")] 28 | [DataRow(".Class", "", DisplayName = "A value starting with a dot should only return an empty string")] 29 | [DataRow(".Namespace.Class", "Namespace", DisplayName = "A value starting with a dot should only return the namespace name")] 30 | [DataRow("Namespace.Class", "Namespace", DisplayName = "A generic class in a namespace should return the namespace name")] 31 | [DataRow("Namespace.Namespace.Namespace.Class", "Namespace.Namespace.Namespace", DisplayName = "A value with a hiearchy of namespaces should return all namespace parts")] 32 | [TestMethod] 33 | public void ReduceAFullTypeNameToOnlyTheNamespacePart(string fullname, string expectation) 34 | { 35 | // Act 36 | var result = fullname.Namespace(); 37 | 38 | // Assert 39 | result.Should().Be(expectation); 40 | } 41 | 42 | [DataRow(null, 0, DisplayName = "A `null` value should return an empty list")] 43 | [DataRow("", 0, DisplayName = "An empty string value should return an empty list")] 44 | [DataRow(".", 0, DisplayName = "A value with only a dot should return an empty list")] 45 | [DataRow("Class", 1, DisplayName = "A class name without a namespace should return a list with a single item")] 46 | [DataRow("Namespace.Class", 2, DisplayName = "A class in a namespace should return a list with a two item")] 47 | [DataRow(".Class", 1, DisplayName = "A value starting with a dot should return a list with a single item")] 48 | [DataRow(".Namespace.Class", 2, DisplayName = "A value starting with a dot should return a list with a two item")] 49 | [DataRow("Namespace.Class", 2, DisplayName = "A generic class in a namespace should return a list with a single item")] 50 | [DataRow("Namespace.Namespace.Namespace.Class", 4, DisplayName = "A value with a hiearchy of namespaces should return a list with four items")] 51 | [TestMethod] 52 | public void AllValidNamespacePartsShouldBeReturned(string fullname, int expectation) 53 | { 54 | // Act 55 | var result = fullname.NamespaceParts(); 56 | 57 | // Assert 58 | result.Should().HaveCount(expectation); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Analyzer.Tests/MethodModifierTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Analyzer.Tests; 2 | 3 | [TestClass] 4 | public class MethodModifierTests 5 | { 6 | [DataRow("void Method() {}", Modifier.Private, DisplayName = "A method description about a method without a modifier should contain the `private` modifier")] 7 | [DataRow("private void Method() {}", Modifier.Private, DisplayName = "A method description about a `private` method should contain the `private` modifier")] 8 | [DataRow("public void Method() {}", Modifier.Public, DisplayName = "A method description about a `public` method should contain the `public` modifier")] 9 | [DataRow("protected void Method() {}", Modifier.Protected, DisplayName = "A method description about a `protected` method should contain the `protected` modifier")] 10 | [DataRow("internal void Method() {}", Modifier.Internal, DisplayName = "A method description about a `internal` method should contain the `internal` modifier")] 11 | [DataRow("protected internal void Method() {}", Modifier.Protected | Modifier.Internal, DisplayName = "A method description about a `protected internal` method should contain the `protected` and `internal` modifiers")] 12 | [DataRow("private protected void Method() {}", Modifier.Private | Modifier.Protected, DisplayName = "A method description about a `private protected` method should contain the `private` and `protected` modifiers")] 13 | [TestMethod] 14 | public void MethodsShouldHaveTheCorrectAccessModifiers(string method, Modifier modifier) 15 | { 16 | // Assign 17 | var source = @$" 18 | class Test 19 | {{ 20 | {method} 21 | }} 22 | "; 23 | 24 | // Act 25 | var types = TestHelper.VisitSyntaxTree(source); 26 | 27 | // Assert 28 | types[0].Methods[0].Modifiers.Should().Be(modifier); 29 | } 30 | 31 | [TestMethod] 32 | [DataRow("public abstract void Method();", Modifier.Abstract, DisplayName = "A method description about an `abstract` method should contain the `abstract` modifier")] 33 | [DataRow("async void Method() {}", Modifier.Async, DisplayName = "A method description about an `async` method should contain the `async` modifier")] 34 | [DataRow("extern void Method();", Modifier.Extern, DisplayName = "A method description about an `extern` method should contain the `extern` modifier")] 35 | [DataRow("new void MethodB() {}", Modifier.New, DisplayName = "A method description about a `new` method should contain the `new` modifier")] 36 | [DataRow("partial void Method();", Modifier.Partial, DisplayName = "A method description about a `partial` method should contain the `partial` modifier")] 37 | [DataRow("protected override void MethodB() {}", Modifier.Override, DisplayName = "A method description about an `override` method should contain the `override` modifier")] 38 | [DataRow("sealed protected override void MethodB() {}", Modifier.Sealed, DisplayName = "A method description about a `sealed` method should contain the `sealed` modifier")] 39 | [DataRow("static void Method() {}", Modifier.Static, DisplayName = "A method description about a `static` method should contain the `static` modifier")] 40 | [DataRow("unsafe void Method() {}", Modifier.Unsafe, DisplayName = "A method description about an `unsafe` method should contain the `unsafe` modifier")] 41 | [DataRow("protected virtual void Method() {}", Modifier.Virtual, DisplayName = "A method description about a `virtual` method should contain the `virtual` modifier")] 42 | public void MethodsShouldHaveTheCorrectModifiers(string method, Modifier modifier) 43 | { 44 | // Assign 45 | var source = @$" 46 | abstract partial class Test : ClassB 47 | {{ 48 | {method} 49 | }} 50 | abstract class ClassB {{ 51 | protected virtual void MethodB() {{}} 52 | }} 53 | "; 54 | 55 | // Act 56 | var types = TestHelper.VisitSyntaxTree(source, "CS0626", "CS1998"); 57 | 58 | // Assert 59 | types[0].Methods[0].Modifiers.Should().HaveFlag(modifier); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Analyzer/Extensions/ExpressionSyntaxExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | public static class ExpressionSyntaxExtensions 4 | { 5 | public static string ResolveValue(this ExpressionSyntax expression, SemanticModel semanticModel) 6 | { 7 | return expression switch 8 | { 9 | IdentifierNameSyntax identifier when semanticModel.GetSymbolInfo(identifier).Symbol is IFieldSymbol field && field.IsConst => field.ConstantValue!.ToString()!, 10 | IdentifierNameSyntax identifier => identifier.Identifier.ValueText, 11 | InterpolatedStringExpressionSyntax interpolatedString => interpolatedString.Contents.ToString(), 12 | LiteralExpressionSyntax literal => literal.Token.ValueText, 13 | BinaryExpressionSyntax binary when binary.Right is InterpolatedStringExpressionSyntax || binary.Right is LiteralExpressionSyntax => ConcatBinaryExpression(binary), 14 | ArrayCreationExpressionSyntax arrayCreation => FormatArrayCreation(arrayCreation), 15 | ObjectCreationExpressionSyntax objectCreation => FormatObjectCreation(objectCreation), 16 | _ => expression.ToString() 17 | }; 18 | } 19 | 20 | /// 21 | /// Format Object initializers to a single line, removing newlines and indenting. 22 | /// 23 | private static string FormatObjectCreation(ObjectCreationExpressionSyntax objectCreation) 24 | { 25 | var initializer = objectCreation.Initializer; 26 | if (initializer is not null) 27 | { 28 | var expressions = new SeparatedSyntaxList(); 29 | 30 | foreach (var expression in initializer.Expressions) 31 | { 32 | expressions = expressions.Add(expression.WithoutTrivia().WithLeadingTrivia(SyntaxFactory.Space)); 33 | } 34 | 35 | initializer = initializer.Update(initializer.OpenBraceToken.WithoutTrivia(), expressions, initializer.CloseBraceToken.WithLeadingTrivia(SyntaxFactory.Space)); 36 | } 37 | 38 | return objectCreation.Update(objectCreation.NewKeyword, objectCreation.Type.WithTrailingTrivia(SyntaxFactory.Space), objectCreation.ArgumentList, initializer).ToString(); 39 | } 40 | 41 | /// 42 | /// Format Array initializers to a single line, removing newlines and indenting. 43 | /// 44 | private static string FormatArrayCreation(ArrayCreationExpressionSyntax arrayCreation) 45 | { 46 | var initializer = arrayCreation.Initializer; 47 | if (initializer is not null) 48 | { 49 | var expressions = new SeparatedSyntaxList(); 50 | 51 | foreach (var expression in initializer.Expressions) 52 | { 53 | expressions = expressions.Add(expression.WithLeadingTrivia(SyntaxFactory.Space)); 54 | } 55 | 56 | initializer = initializer.Update(initializer.OpenBraceToken.WithoutTrivia(), expressions, initializer.CloseBraceToken); 57 | } 58 | 59 | return arrayCreation.Update(arrayCreation.NewKeyword, arrayCreation.Type, initializer).ToString(); 60 | } 61 | 62 | /// 63 | /// Format String concatination to a single string, removing newlines and indenting. 64 | /// 65 | private static string ConcatBinaryExpression(BinaryExpressionSyntax binary) 66 | { 67 | var parts = new Stack(); 68 | 69 | AddPart(binary.Right); 70 | 71 | var left = binary.Left; 72 | 73 | while (left is BinaryExpressionSyntax binaryExpression && binaryExpression.IsKind(SyntaxKind.AddExpression)) 74 | { 75 | AddPart(binaryExpression.Right); 76 | 77 | left = binaryExpression.Left; 78 | } 79 | 80 | AddPart(left); 81 | 82 | return string.Join(string.Empty, parts); 83 | 84 | void AddPart(ExpressionSyntax binary) 85 | { 86 | switch (binary) 87 | { 88 | case InterpolatedStringExpressionSyntax interpolatedRight: 89 | parts.Push(interpolatedRight.Contents.ToFullString()); 90 | break; 91 | case LiteralExpressionSyntax literalRight: 92 | parts.Push(literalRight.Token.ValueText); 93 | break; 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Analyzer.Tests/FieldDeclarationTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Analyzer.Tests; 2 | 3 | [TestClass] 4 | public class FieldDeclarationTests 5 | { 6 | [DataRow("string field;", Modifier.Private, DisplayName = "A field description about a field without a modifier should contain the `private` modifier")] 7 | [DataRow("private string field;", Modifier.Private, DisplayName = "A field description about a `private` field should contain the `private` modifier")] 8 | [DataRow("public string field;", Modifier.Public, DisplayName = "A field description about a `public` field should contain the `public` modifier")] 9 | [DataRow("protected string field;", Modifier.Protected, DisplayName = "A field description about a `protected` field should contain the `protected` modifier")] 10 | [DataRow("internal string field = default;", Modifier.Internal, DisplayName = "A field description about a `internal` field should contain the `internal` modifier")] 11 | [DataRow("protected internal string field;", Modifier.Protected | Modifier.Internal, DisplayName = "A field description about a `protected internal` field should contain the `protected` and `internal` modifiers")] 12 | [DataRow("private protected string field;", Modifier.Private | Modifier.Protected, DisplayName = "A field description about a `private protected` field should contain the `private` and `protected` modifiers")] 13 | [TestMethod] 14 | public void FieldsShouldHaveTheCorrectAccessModifiers(string field, Modifier modifier) 15 | { 16 | // Assign 17 | var source = @$" 18 | public class Test 19 | {{ 20 | {field} 21 | }} 22 | "; 23 | 24 | // Act 25 | var types = TestHelper.VisitSyntaxTree(source, "CS0169"); 26 | 27 | // Assert 28 | types[0].Fields[0].Modifiers.Should().Be(modifier); 29 | } 30 | 31 | [TestMethod] 32 | [DataRow("public static string field;", Modifier.Static, DisplayName = "A field description about a `static` field should contain the `static` modifier")] 33 | [DataRow("public unsafe string field;", Modifier.Unsafe, DisplayName = "A field description about an `unsafe` field should contain the `unsafe` modifier")] 34 | public void FieldsShouldHaveTheCorrectModifiers(string method, Modifier modifier) 35 | { 36 | // Assign 37 | var source = @$" 38 | public class Test 39 | {{ 40 | {method} 41 | }} 42 | "; 43 | 44 | // Act 45 | var types = TestHelper.VisitSyntaxTree(source); 46 | 47 | // Assert 48 | types[0].Fields[0].Modifiers.Should().HaveFlag(modifier); 49 | } 50 | 51 | [TestMethod] 52 | public void MultipleFieldDeclarationsShouldCreateAFieldDescriptionPerField() 53 | { 54 | // Assign 55 | var source = @" 56 | public class Test 57 | { 58 | public string field1, field2; 59 | } 60 | "; 61 | 62 | // Act 63 | var types = TestHelper.VisitSyntaxTree(source); 64 | 65 | // Assert 66 | types[0].Fields.Should().HaveCount(2); 67 | } 68 | 69 | [TestMethod] 70 | public void AFieldDeclarationWithAnInitializerShouldBeRecognizedAndParsed() 71 | { 72 | // Assign 73 | var source = @" 74 | public class Test 75 | { 76 | public string field1 = ""value""; 77 | } 78 | "; 79 | 80 | // Act 81 | var types = TestHelper.VisitSyntaxTree(source); 82 | 83 | // Assert 84 | types[0].Fields[0].HasInitializer.Should().BeTrue(); 85 | types[0].Fields[0].Initializer.Should().Be("value"); 86 | } 87 | 88 | [TestMethod] 89 | public void AFieldInitializedWithAConstantShouldBeResolvedToTheActualConstValue() 90 | { 91 | // Assign 92 | var source = @" 93 | public class Test 94 | { 95 | private const string CONST = ""value""; 96 | public string field1 = CONST; 97 | } 98 | "; 99 | 100 | // Act 101 | var types = TestHelper.VisitSyntaxTree(source); 102 | 103 | // Assert 104 | types[0].Fields[0].Initializer.Should().Be("value"); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Serializations.Tests/SerializationTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using Newtonsoft.Json; 4 | 5 | namespace LivingDocumentation.Analyzer.Tests 6 | { 7 | [TestClass] 8 | public class SerializationTests 9 | { 10 | [TestMethod] 11 | public void NoTypes_Should_GiveEmptyArray() 12 | { 13 | // Assign 14 | var source = @"namespace Test {}"; 15 | 16 | // Act 17 | var types = TestHelper.VisitSyntaxTree(source); 18 | 19 | var result = JsonConvert.SerializeObject(types, JsonDefaults.SerializerSettings()); 20 | 21 | // Assert 22 | result.Should().Be("[]"); 23 | } 24 | 25 | [TestMethod] 26 | public void InternalClass_Should_GiveOnlyFullName() 27 | { 28 | // Assign 29 | var source = @"class Test {}"; 30 | 31 | // Act 32 | var types = TestHelper.VisitSyntaxTree(source); 33 | 34 | var result = JsonConvert.SerializeObject(types, JsonDefaults.SerializerSettings()); 35 | 36 | // Assert 37 | result.Should().Be(@"[{""FullName"":""Test""}]"); 38 | } 39 | 40 | [TestMethod] 41 | public void PublicClass_Should_GiveOnlyNonDefaultModifier() 42 | { 43 | // Assign 44 | var source = @"public class Test {}"; 45 | 46 | // Act 47 | var types = TestHelper.VisitSyntaxTree(source); 48 | 49 | var result = JsonConvert.SerializeObject(types, JsonDefaults.SerializerSettings()); 50 | 51 | // Assert 52 | result.Should().Be(@"[{""FullName"":""Test"",""Modifiers"":2}]"); 53 | } 54 | 55 | [TestMethod] 56 | public void PrivateVoidMethod_Should_GiveOnlyName() 57 | { 58 | // Assign 59 | var source = @"class Test { 60 | void Method() {} 61 | }"; 62 | 63 | // Act 64 | var types = TestHelper.VisitSyntaxTree(source); 65 | 66 | var result = JsonConvert.SerializeObject(types, JsonDefaults.SerializerSettings()); 67 | 68 | // Assert 69 | result.Should().Be(@"[{""FullName"":""Test"",""Methods"":[{""Name"":""Method""}]}]"); 70 | } 71 | 72 | [TestMethod] 73 | public void PrivateNonVoidMethod_Should_GiveNameAndReturnType() 74 | { 75 | // Assign 76 | var source = @"class Test { 77 | int Method() { return 0; } 78 | }"; 79 | 80 | // Act 81 | var types = TestHelper.VisitSyntaxTree(source); 82 | 83 | var result = JsonConvert.SerializeObject(types, JsonDefaults.SerializerSettings()); 84 | 85 | // Assert 86 | result.Should().Match(@"[{""FullName"":""Test"",""Methods"":[{""Name"":""Method"",""ReturnType"":""int"",*}]}]"); 87 | } 88 | 89 | [TestMethod] 90 | public void Attributes_Should_GiveNameAndType() 91 | { 92 | // Assign 93 | var source = @" 94 | [System.Obsolete] 95 | class Test { 96 | }"; 97 | 98 | // Act 99 | var types = TestHelper.VisitSyntaxTree(source); 100 | 101 | var result = JsonConvert.SerializeObject(types, JsonDefaults.SerializerSettings()); 102 | 103 | // Assert 104 | result.Should().Match(@"[{""FullName"":""Test"",""Attributes"":[{""Type"":""System.ObsoleteAttribute"",""Name"":""System.Obsolete""}]}]"); 105 | } 106 | 107 | [TestMethod] 108 | public void AttributeArguments_Should_GiveName_TypeAndValue() 109 | { 110 | // Assign 111 | var source = @" 112 | [System.Obsolete(""Reason"")] 113 | class Test { 114 | }"; 115 | 116 | // Act 117 | var types = TestHelper.VisitSyntaxTree(source); 118 | 119 | var result = JsonConvert.SerializeObject(types, JsonDefaults.SerializerSettings()); 120 | 121 | // Assert 122 | result.Should().Match(@"[{""FullName"":""Test"",""Attributes"":[{""Type"":""System.ObsoleteAttribute"",""Name"":""System.Obsolete"",""Arguments"":[{""Name"":""Reason"",""Type"":""string"",""Value"":""Reason""}]}]}]"); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Living Documentation 2 | 3 | ![Azure DevOps builds](https://img.shields.io/azure-devops/build/hompus/dccc1034-d776-48ea-8a70-8822a02987f9/6?style=plastic) ![Azure DevOps tests](https://img.shields.io/azure-devops/tests/hompus/LivingDocumentation/6?style=plastic) 4 | 5 | Living Documentation allows you to analyze your dotnet source code and generate comprehensive documentation for your stakeholders. 6 | It's a powerful tool that bridges the gap between code and documentation, ensuring that your documentation is always up-to-date with your source code. 7 | 8 | ## Features 9 | 10 | * **Analyzer**: A tool to analyze dotnet projects or solutions. 11 | * **Libraries**: Assists in generating applications that can create plain text files such as MarkDown, AsciiDoc, PlantUML, Mermaid, and more. 12 | 13 | ## Packages 14 | 15 | | Package | Type | Status | 16 | | ------------- | -------- | ------------------------------------------------- | 17 | | Analyzer Tool | Released | [![Nuget][NUGET_BADGE]][NUGET_FEED] | 18 | | Analyzer Tool | Preview | [![Azure Artifacts][PREVIEW_BADGE]][PREVIEW_FEED] | 19 | 20 | ## Presentation 21 | 22 | Watch the session given at NDC London 2023 that covers examples using this tool: 23 | 24 | [![Use your source code to document your application - Michaël Hompus - NDC London 2023](https://img.youtube.com/vi/hf8hzGb2C6E/0.jpg)](https://www.youtube.com/watch?v=hf8hzGb2C6E) 25 | 26 | ## Getting Started 27 | 28 | ### Prerequisites 29 | 30 | * Dotnet 6.0 SDK or newer. 31 | 32 | ## Installation 33 | 34 | Install the analyzer as a dotnet global tool: 35 | 36 | ```shell 37 | dotnet tool install --global LivingDocumentation.Analyzer 38 | ``` 39 | 40 | ## Generating Documentation 41 | 42 | Using LivingDocumentation to generate documentation involves a three-step process: 43 | 44 | 1. **Analyze Source Code**: Run the LivingDocumentation.Analyzer with your Visual Studio solution file as input. 45 | This will generate an intermediate JSON file containing detailed information about your source code. 46 | 2. **Develop Renderers**: Create a custom "render application" to interpret the JSON file and generate various views on your source code, such as class diagrams or sequence diagrams. 47 | 3. **Output Documentation**: Export your findings in text-based formats like Markdown, AsciiDoc, PlantUML, Mermaid, etc. 48 | 49 | Both during local development, as during your CI&CD pipeline, can follow the same flow. 50 | 51 | ### Local Development 52 | 53 | The analysis of a solution might take some time. 54 | Therefore, an intermediate JSON file is created to speed up the documentation generation process. 55 | This ensures a fast feedback loop when developing your renderers. 56 | 57 | ## Develop Your Own Renderers 58 | 59 | A renderer application can be as simple as a command line tool that takes in the generated JSON files, makes conclusions based on the type information and writes this to a plain text file format. 60 | 61 | To get started quickly, you should make a dependency on 2 NuGet packages in your project: 62 | 63 | * **LivingDocumentation.RenderExtensions**: Contains extension methods and dependencies for serialized analysis. 64 | * **LivingDocumentation.Json**: Contains JSON serializers and contract resolvers. 65 | 66 | ## More details 67 | 68 | More details can be found in the [Guide](docs/guide.md). 69 | 70 | ## Dive Deeper 71 | 72 | For more detailed examples and advanced use cases, refer to the second chapter of the [LivingDocumentation.Workshop](https://github.com/eNeRGy164/LivingDocumentation.Workshop/). 73 | 74 | ## Contributing 75 | 76 | You are welcome to contribute! Feel free to create [issues](https://github.com/eNeRGy164/LivingDocumentation/issues) or [pull requests](https://github.com/eNeRGy164/LivingDocumentation/pulls). 77 | 78 | ## License 79 | 80 | This project is licensed under the [MIT License](LICENSE). 81 | 82 | [NUGET_BADGE]: https://img.shields.io/nuget/v/LivingDocumentation.Analyzer.svg?style=plastic 83 | [NUGET_FEED]: https://www.nuget.org/packages/LivingDocumentation.Analyzer/ 84 | [PREVIEW_BADGE]: https://feeds.dev.azure.com/hompus/dccc1034-d776-48ea-8a70-8822a02987f9/_apis/public/Packaging/Feeds/030d64ca-8fad-4972-b7b7-8b1679c95e25/Packages/f3b0fbae-213f-412b-a98c-4d339e7a09e7/Badge 85 | [PREVIEW_FEED]: https://dev.azure.com/hompus/LivingDocumentation/_packaging?_a=package&feed=030d64ca-8fad-4972-b7b7-8b1679c95e25&package=f3b0fbae-213f-412b-a98c-4d339e7a09e7&preferRelease=true 86 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Analyzer.Tests/ClassModifierTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Analyzer.Tests; 2 | 3 | [TestClass] 4 | public class ClassModifierTests 5 | { 6 | [DataRow("class Test {}", Modifier.Internal, DisplayName = "A type description about a class without a modifier should contain the `internal` modifier")] 7 | [DataRow("public class Test {}", Modifier.Public, DisplayName = "A type description about a `public` class should contain the `public` modifier")] 8 | [DataRow("internal class Test {}", Modifier.Internal, DisplayName = "A type description about an `internal` class should contain the `internal` modifier")] 9 | [TestMethod] 10 | public void ClassesShouldHaveTheCorrectAccessModifiers(string @class, Modifier modifier) 11 | { 12 | // Assign 13 | var source = @class; 14 | 15 | // Act 16 | var types = TestHelper.VisitSyntaxTree(source); 17 | 18 | // Assert 19 | types[0].Modifiers.Should().Be(modifier); 20 | } 21 | 22 | [TestMethod] 23 | [DataRow("public abstract class Test {}", Modifier.Abstract, DisplayName = "A type description about an `abstract` class should contain the `abstract` modifier")] 24 | [DataRow("static class Test {}", Modifier.Static, DisplayName = "A type description about a `static` class should contain the `static` modifier")] 25 | [DataRow("unsafe class Test {}", Modifier.Unsafe, DisplayName = "A type description about an `unsafe` class should contain the `unsafe` modifier")] 26 | public void ClassesShouldHaveTheCorrectModifiers(string @class, Modifier modifier) 27 | { 28 | // Assign 29 | var source = @class; 30 | 31 | // Act 32 | var types = TestHelper.VisitSyntaxTree(source, "CS0067", "CS0626", "CS1998"); 33 | 34 | // Assert 35 | types[0].Modifiers.Should().HaveFlag(modifier); 36 | } 37 | 38 | [DataRow("class NestedTest {}", Modifier.Private, DisplayName = "A type description about a nested class without a modifier should contain the `private` modifier")] 39 | [DataRow("private class NestedTest {}", Modifier.Private, DisplayName = "A type description about a `private` nested class should contain the `private` modifier")] 40 | [DataRow("public class NestedTest {}", Modifier.Public, DisplayName = "A type description about a `public` nested class should contain the `public` modifier")] 41 | [DataRow("protected class NestedTest {}", Modifier.Protected, DisplayName = "A type description about a `protected` nested class should contain the `protected` modifier")] 42 | [DataRow("internal class NestedTest {}", Modifier.Internal, DisplayName = "A type description about an `internal` nested class should contain the `internal` modifier")] 43 | [DataRow("protected internal class NestedTest {}", Modifier.Protected | Modifier.Internal, DisplayName = "A type description about a `protected internal` nested class should contain the `protected` and `internal` modifiers")] 44 | [DataRow("private protected class NestedTest {}", Modifier.Private | Modifier.Protected, DisplayName = "A type description about a `private protected` nested class should contain the `private` and `protected` modifiers")] 45 | [TestMethod] 46 | public void NestedClassesShouldHaveTheCorrectAccessModifiers(string @class, Modifier modifier) 47 | { 48 | // Assign 49 | var source = @$" 50 | class Test 51 | {{ 52 | {@class} 53 | }} 54 | "; 55 | 56 | // Act 57 | var types = TestHelper.VisitSyntaxTree(source, "CS0067"); 58 | 59 | // Assert 60 | types[1].Modifiers.Should().Be(modifier); 61 | } 62 | 63 | [TestMethod] 64 | [DataRow("public abstract class NestedTest {}", Modifier.Abstract, DisplayName = "A type description about an `abstract` nested class should contain the `abstract` modifier")] 65 | [DataRow("new class NestedB {}", Modifier.New, DisplayName = "A type description about a `new` nested class should contain the `new` modifier")] 66 | [DataRow("partial class NestedTest {}", Modifier.Partial, DisplayName = "A type description about a `partial` nested class should contain the `partial` modifier")] 67 | [DataRow("static class NestedTest {}", Modifier.Static, DisplayName = "A type description about a `static` nested class should contain the `static` modifier")] 68 | [DataRow("unsafe class NestedTest {}", Modifier.Unsafe, DisplayName = "A type description about an `unsafe` nested class should contain the `unsafe` modifier")] 69 | public void NestedClassesShouldHaveTheCorrectModifiers(string @class, Modifier modifier) 70 | { 71 | // Assign 72 | var source = @$" 73 | class Test : ClassB 74 | {{ 75 | {@class} 76 | }} 77 | class ClassB {{ 78 | internal class NestedB {{}}; 79 | }} 80 | "; 81 | 82 | // Act 83 | var types = TestHelper.VisitSyntaxTree(source, "CS0067"); 84 | 85 | // Assert 86 | types[1].Modifiers.Should().HaveFlag(modifier); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /samples/LivingDocumentation.eShopOnContainers/AggregateRenderer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text; 5 | using LivingDocumentation.Uml; 6 | using PlantUml.Builder; 7 | using PlantUml.Builder.ClassDiagrams; 8 | 9 | namespace LivingDocumentation.eShopOnContainers 10 | { 11 | public class AggregateRenderer 12 | { 13 | public IReadOnlyDictionary Render() 14 | { 15 | var files = new Dictionary(); 16 | 17 | var aggregates = Program.Types.Where(t => t.IsAggregateRoot()).ToList(); 18 | 19 | foreach (var aggregate in aggregates) 20 | { 21 | var aggregateName = aggregate.Name; 22 | 23 | var stringBuilder = new StringBuilder(); 24 | stringBuilder.UmlDiagramStart(); 25 | stringBuilder.SkinParameter(SkinParameter.MinClassWidth, "160"); 26 | stringBuilder.SkinParameter(SkinParameter.Linetype, "ortho"); 27 | stringBuilder.NamespaceStart(aggregateName, stereotype: "aggregate"); 28 | 29 | var rootBuilder = this.RenderClass(aggregate); 30 | stringBuilder.Append(rootBuilder); 31 | 32 | stringBuilder.NamespaceEnd(); 33 | stringBuilder.UmlDiagramEnd(); 34 | 35 | var fileName = $"aggregate.{aggregateName.ToLowerInvariant()}.puml"; 36 | files.Add(aggregate.FullName, fileName); 37 | 38 | File.WriteAllText(fileName, stringBuilder.ToString()); 39 | } 40 | 41 | return files; 42 | } 43 | 44 | private StringBuilder RenderClass(TypeDescription type) 45 | { 46 | StringBuilder stringBuilder = new StringBuilder(); 47 | 48 | if (type.IsEnumeration()) 49 | { 50 | stringBuilder.EnumStart(type.Name, stereotype: "enumeration"); 51 | 52 | foreach (var field in type.Fields) 53 | { 54 | stringBuilder.InlineClassMember(new ClassMember(field.Name)); 55 | } 56 | 57 | stringBuilder.EnumEnd(); 58 | } 59 | else 60 | { 61 | if (type.IsAggregateRoot()) 62 | { 63 | stringBuilder.ClassStart(type.Name, isAbstract: type.IsAbstract(), stereotype: "root", customSpot: new CustomSpot('R', NamedColor.LightBlue)); 64 | } 65 | 66 | if (type.IsValueObject()) 67 | { 68 | stringBuilder.ClassStart(type.Name, isAbstract: type.IsAbstract(), stereotype: "value object", customSpot: new CustomSpot('O', NamedColor.Wheat)); 69 | } 70 | 71 | if (type.IsEntity()) 72 | { 73 | stringBuilder.ClassStart(type.Name, isAbstract: type.IsAbstract(), stereotype: "entity"); 74 | } 75 | 76 | foreach (var property in type.Properties.Where(p => !p.IsPrivate())) 77 | { 78 | stringBuilder.InlineClassMember(new ClassMember(property.Name, isAbstract: property.IsAbstract(), isStatic: property.IsStatic(), visibility: property.ToUmlVisibility())); 79 | } 80 | 81 | foreach (var method in type.Methods.Where(m => !m.IsPrivate())) 82 | { 83 | var fullMethod = $"{method.Name}({string.Join(", ", method.Parameters.Select(s => s.Name))})"; 84 | stringBuilder.InlineClassMember(new ClassMember(fullMethod, isAbstract: method.IsAbstract(), isStatic: method.IsStatic(), visibility: method.ToUmlVisibility())); 85 | } 86 | 87 | stringBuilder.ClassEnd(); 88 | } 89 | 90 | foreach (var propertyDescription in type.Properties) 91 | { 92 | var property = Program.Types.FirstOrDefault(t => string.Equals(t.FullName, propertyDescription.Type) || (propertyDescription.Type.IsEnumerable() && string.Equals(t.FullName, propertyDescription.Type.GenericTypes().First()))); 93 | if (property != null) 94 | { 95 | var classBuilder = this.RenderClass(property); 96 | stringBuilder.Append(classBuilder); 97 | 98 | // Relation 99 | if (propertyDescription.Type.IsEnumerable()) 100 | { 101 | stringBuilder.Relationship(type.Name, "--", property.Name, label: "1..*"); 102 | } 103 | else 104 | { 105 | stringBuilder.Relationship(type.Name, "--", property.Name); 106 | } 107 | } 108 | } 109 | 110 | return stringBuilder; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Analyzer.Tests/EventDeclarationTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Analyzer.Tests; 2 | 3 | [TestClass] 4 | public class EventDeclarationTests 5 | { 6 | [DataRow("event System.Action @event;", Modifier.Private, DisplayName = "An event description about an event without a modifier should contain the `private` modifier")] 7 | [DataRow("private event System.Action @event;", Modifier.Private, DisplayName = "An event description about a `private` event should contain the `private` modifier")] 8 | [DataRow("public event System.Action @event;", Modifier.Public, DisplayName = "An event description about a `public` event should contain the `public` modifier")] 9 | [DataRow("protected event System.Action @event;", Modifier.Protected, DisplayName = "An event description about a `protected` event should contain the `protected` modifier")] 10 | [DataRow("internal event System.Action @event;", Modifier.Internal, DisplayName = "An event description about an `internal` event should contain the `internal` modifier")] 11 | [DataRow("protected internal event System.Action @event;", Modifier.Protected | Modifier.Internal, DisplayName = "An event description about a `protected internal` event should contain the `protected` and `internal` modifiers")] 12 | [DataRow("private protected event System.Action @event;", Modifier.Private | Modifier.Protected, DisplayName = "An event description about a `private protected` event should contain the `private` and `protected` modifiers")] 13 | [TestMethod] 14 | public void EventsShouldHaveTheCorrectAccessModifiers(string @event, Modifier modifier) 15 | { 16 | // Assign 17 | var source = @$" 18 | class Test 19 | {{ 20 | {@event} 21 | }} 22 | "; 23 | 24 | // Act 25 | var types = TestHelper.VisitSyntaxTree(source, "CS0067"); 26 | 27 | // Assert 28 | types[0].Events[0].Modifiers.Should().Be(modifier); 29 | } 30 | 31 | [TestMethod] 32 | [DataRow("public abstract event System.Action @event;", Modifier.Abstract, DisplayName = "An event description about an `abstract` event should contain the `abstract` modifier")] 33 | [DataRow("extern event System.Action @event;", Modifier.Extern, DisplayName = "An event description about an `extern` event should contain the `extern` modifier")] 34 | [DataRow("new event System.Action EventB;", Modifier.New, DisplayName = "An event description about a `new` event should contain the `new` modifier")] 35 | [DataRow("protected override event System.Action EventB;", Modifier.Override, DisplayName = "An event description about an `override` event should contain the `override` modifier")] 36 | [DataRow("sealed protected override event System.Action EventB;", Modifier.Sealed, DisplayName = "An event description about a `sealed` event should contain the `sealed` modifier")] 37 | [DataRow("static event System.Action @event;", Modifier.Static, DisplayName = "An event description about a `static` event should contain the `static` modifier")] 38 | [DataRow("unsafe event System.Action @event;", Modifier.Unsafe, DisplayName = "An event description about an `unsafe` event should contain the `unsafe` modifier")] 39 | [DataRow("protected virtual event System.Action @event;", Modifier.Virtual, DisplayName = "An event description about a `virtual` event should contain the `virtual` modifier")] 40 | public void EventsShouldHaveTheCorrectModifiers(string @event, Modifier modifier) 41 | { 42 | // Assign 43 | var source = @$" 44 | abstract partial class Test : ClassB 45 | {{ 46 | {@event} 47 | }} 48 | abstract class ClassB {{ 49 | protected virtual event System.Action EventB; 50 | }} 51 | "; 52 | 53 | // Act 54 | var types = TestHelper.VisitSyntaxTree(source, "CS0067", "CS0626", "CS1998"); 55 | 56 | // Assert 57 | types[0].Events[0].Modifiers.Should().HaveFlag(modifier); 58 | } 59 | 60 | [TestMethod] 61 | public void MultipleEventDeclarationsShouldCreateAnEventDescriptionPerEvent() 62 | { 63 | // Assign 64 | var source = @" 65 | class Test 66 | { 67 | event System.Action event1, event2; 68 | } 69 | "; 70 | 71 | // Act 72 | var types = TestHelper.VisitSyntaxTree(source, "CS0067"); 73 | 74 | // Assert 75 | types[0].Events.Should().HaveCount(2); 76 | } 77 | 78 | [TestMethod] 79 | public void EventDeclaredWithAnInitialValueShouldBeRecognizedAndParsed() 80 | { 81 | // Assign 82 | var source = @" 83 | public class Test 84 | { 85 | event System.Action @event = () => {}; 86 | } 87 | "; 88 | 89 | // Act 90 | var types = TestHelper.VisitSyntaxTree(source); 91 | 92 | // Assert 93 | types[0].Events[0].HasInitializer.Should().BeTrue(); 94 | types[0].Events[0].Initializer.Should().Be("() => {}"); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.Extensions.Tests/IHaveModifiersExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.Extensions.Tests; 2 | 3 | [TestClass] 4 | public partial class IHaveModifiersExtensionsTests 5 | { 6 | [DataRow("IsInternal", Modifier.Public, false, DisplayName = "`IsInternal()` with a non-internal modifier should return `false`")] 7 | [DataRow("IsInternal", Modifier.Internal | Modifier.Static, true, DisplayName = "`IsInternal()` with an internal modifier should return `true`")] 8 | [DataRow("IsPublic", Modifier.Private, false, DisplayName = "`IsPublic()` with a non-public modifier should return `false`")] 9 | [DataRow("IsPublic", Modifier.Public | Modifier.Static, true, DisplayName = "`IsPublic()` with a public modifier should return `true`")] 10 | [DataRow("IsPrivate", Modifier.Public, false, DisplayName = "`IsPrivate()` with a non-private modifier should return `false`")] 11 | [DataRow("IsPrivate", Modifier.Private | Modifier.Static, true, DisplayName = "`IsPrivate()` with a private modifier should return `true`")] 12 | [DataRow("IsProtected", Modifier.Public, false, DisplayName = "`IsProtected()` with a non-protected modifier should return `false`")] 13 | [DataRow("IsProtected", Modifier.Protected | Modifier.Static, true, DisplayName = "`IsProtected()` with a protected modifier should return `true`")] 14 | [DataRow("IsStatic", Modifier.Public, false, DisplayName = "`IsStatic()` with a non-static modifier should return `false`")] 15 | [DataRow("IsStatic", Modifier.Public | Modifier.Static, true, DisplayName = "`IsStatic()` with a static modifier should return `true`")] 16 | [DataRow("IsAbstract", Modifier.Private, false, DisplayName = "`IsAbstract()` with a non-abstract modifier should return `false`")] 17 | [DataRow("IsAbstract", Modifier.Public | Modifier.Abstract, true, DisplayName = "`IsAbstract()` with a abstract modifier should return `true`")] 18 | [DataRow("IsOverride", Modifier.Public, false, DisplayName = "`IsOverride()` with a non-override modifier should return `false`")] 19 | [DataRow("IsOverride", Modifier.Public | Modifier.Override, true, DisplayName = "`IsOverride()` with a override modifier should return `true`")] 20 | [DataRow("IsReadonly", Modifier.Public, false, DisplayName = "`IsReadonly()` with a non-readonly modifier should return `false`")] 21 | [DataRow("IsReadonly", Modifier.Public | Modifier.Readonly, true, DisplayName = "`IsReadonly()` with a readonly modifier should return `true`")] 22 | [DataRow("IsAsync", Modifier.Public, false, DisplayName = "`IsAsync()` with a non-async modifier should return `false`")] 23 | [DataRow("IsAsync", Modifier.Public | Modifier.Async, true, DisplayName = "`IsAsync()` with a async modifier should return `true`")] 24 | [DataRow("IsConst", Modifier.Public, false, DisplayName = "`IsConst()` with a non-const modifier should return `false`")] 25 | [DataRow("IsConst", Modifier.Public | Modifier.Const, true, DisplayName = "`IsConst()` with a const modifier should return `true`")] 26 | [DataRow("IsSealed", Modifier.Public, false, DisplayName = "`IsSealed()` with a non-sealed modifier should return `false`")] 27 | [DataRow("IsSealed", Modifier.Public | Modifier.Sealed, true, DisplayName = "`IsSealed()` with a sealed modifier should return `true`")] 28 | [DataRow("IsVirtual", Modifier.Public, false, DisplayName = "`IsVirtual()` with a non-virtual modifier should return `false`")] 29 | [DataRow("IsVirtual", Modifier.Public | Modifier.Virtual, true, DisplayName = "`IsVirtual()` with a virtual modifier should return `true`")] 30 | [DataRow("IsExtern", Modifier.Public, false, DisplayName = "`IsExtern()` with a non-extern modifier should return `false`")] 31 | [DataRow("IsExtern", Modifier.Public | Modifier.Extern, true, DisplayName = "`IsExtern()` with a extern modifier should return `true`")] 32 | [DataRow("IsNew", Modifier.Public, false, DisplayName = "`IsNew()` with a non-new modifier should return `false`")] 33 | [DataRow("IsNew", Modifier.Public | Modifier.New, true, DisplayName = "`IsNew()` with a new modifier should return `true`")] 34 | [DataRow("IsUnsafe", Modifier.Public, false, DisplayName = "`IsUnsafe()` with a non-unsafe modifier should return `false`")] 35 | [DataRow("IsUnsafe", Modifier.Public | Modifier.Unsafe, true, DisplayName = "`IsUnsafe()` with a unsafe modifier should return `true`")] 36 | [DataRow("IsPartial", Modifier.Public, false, DisplayName = "`IsPartial()` with a non-partial modifier should return `false`")] 37 | [DataRow("IsPartial", Modifier.Public | Modifier.Partial, true, DisplayName = "`IsPartial()` with a partial modifier should return `true`")] 38 | [TestMethod] 39 | public void ModifierMethodsShouldReturnCorrectValues(string methodName, Modifier modifiers, bool expectation) 40 | { 41 | var method = typeof(IHaveModifiersExtensions).GetMethods().Single(m => m.Name == methodName); 42 | var parameters = new[] { new Mod { Modifiers = modifiers } }; 43 | 44 | // Act 45 | var result = (bool)method.Invoke(null, parameters); 46 | 47 | // Assert 48 | result.Should().Be(expectation); 49 | } 50 | 51 | private class Mod : IHaveModifiers 52 | { 53 | public Modifier Modifiers { get; set; } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/LivingDocumentation.Descriptions/TypeDescription.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation; 2 | 3 | [DebuggerDisplay("{Type} {Name,nq} ({Namespace,nq})")] 4 | public class TypeDescription(TypeType type, string? fullName) : IHaveModifiers 5 | { 6 | [JsonProperty(Order = 1, PropertyName = nameof(Fields))] 7 | private readonly List fields = []; 8 | 9 | [JsonProperty(Order = 2, PropertyName = nameof(Constructors))] 10 | private readonly List constructors = []; 11 | 12 | [JsonProperty(Order = 3, PropertyName = nameof(Properties))] 13 | private readonly List properties = []; 14 | 15 | [JsonProperty(Order = 4, PropertyName = nameof(Methods))] 16 | private readonly List methods = []; 17 | 18 | [JsonProperty(Order = 5, PropertyName = nameof(EnumMembers))] 19 | private readonly List enumMembers = []; 20 | 21 | [JsonProperty(Order = 6, PropertyName = nameof(Events))] 22 | private readonly List events = []; 23 | 24 | public TypeType Type { get; } = type; 25 | 26 | public string FullName { get; } = fullName ?? string.Empty; 27 | 28 | public DocumentationCommentsDescription? DocumentationComments { get; set; } 29 | 30 | public List BaseTypes { get; } = []; 31 | 32 | [DefaultValue(Modifier.Internal)] 33 | [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] 34 | public Modifier Modifiers { get; set; } 35 | 36 | [JsonProperty(ItemTypeNameHandling = TypeNameHandling.None)] 37 | [JsonConverter(typeof(ConcreteTypeConverter>))] 38 | public List Attributes { get; } = []; 39 | 40 | [JsonIgnore] 41 | public string Name => this.FullName.ClassName(); 42 | 43 | [JsonIgnore] 44 | public string Namespace => this.FullName.Namespace(); 45 | 46 | [JsonIgnore] 47 | public IReadOnlyList Constructors => this.constructors; 48 | 49 | [JsonIgnore] 50 | public IReadOnlyList Properties => this.properties; 51 | 52 | [JsonIgnore] 53 | public IReadOnlyList Methods => this.methods; 54 | 55 | [JsonIgnore] 56 | public IReadOnlyList Events => this.events; 57 | 58 | [JsonIgnore] 59 | public IReadOnlyList Fields => this.fields; 60 | 61 | [JsonIgnore] 62 | public IReadOnlyList EnumMembers => this.enumMembers; 63 | 64 | public void AddMember(MemberDescription member) 65 | { 66 | switch (member) 67 | { 68 | case ConstructorDescription c: 69 | this.constructors.Add(c); 70 | break; 71 | 72 | case FieldDescription f: 73 | this.fields.Add(f); 74 | break; 75 | 76 | case PropertyDescription p: 77 | this.properties.Add(p); 78 | break; 79 | 80 | case MethodDescription m: 81 | this.methods.Add(m); 82 | break; 83 | 84 | case EnumMemberDescription em: 85 | this.enumMembers.Add(em); 86 | break; 87 | 88 | case EventDescription e: 89 | this.events.Add(e); 90 | break; 91 | 92 | default: 93 | throw new NotSupportedException($"Unable to add {member.GetType()} as member"); 94 | } 95 | } 96 | 97 | public override bool Equals(object? obj) 98 | { 99 | if (obj is not TypeDescription other) 100 | { 101 | return false; 102 | } 103 | 104 | return string.Equals(this.FullName, other.FullName); 105 | } 106 | 107 | public override int GetHashCode() => this.FullName.GetHashCode(); 108 | 109 | public IEnumerable MethodBodies() => this.Constructors.Cast().Concat(this.Methods); 110 | 111 | public bool ImplementsType(string fullName) => this.BaseTypes.Contains(fullName); 112 | 113 | public bool ImplementsTypeStartsWith(string partialName) => this.BaseTypes.Any(bt => bt.StartsWith(partialName, StringComparison.Ordinal)); 114 | 115 | public bool IsClass() => this.Type == TypeType.Class; 116 | 117 | public bool IsEnum() => this.Type == TypeType.Enum; 118 | 119 | public bool IsInterface() => this.Type == TypeType.Interface; 120 | 121 | public bool IsStruct() => this.Type == TypeType.Struct; 122 | 123 | public bool HasProperty(string name) => this.properties.Any(m => string.Equals(m.Name, name, StringComparison.Ordinal)); 124 | 125 | public bool HasMethod(string name) => this.methods.Any(m => string.Equals(m.Name, name, StringComparison.Ordinal)); 126 | 127 | public bool HasEvent(string name) => this.events.Any(m => string.Equals(m.Name, name, StringComparison.Ordinal)); 128 | 129 | public bool HasField(string name) => this.fields.Any(m => string.Equals(m.Name, name, StringComparison.Ordinal)); 130 | 131 | public bool HasEnumMember(string name) => this.enumMembers.Any(m => string.Equals(m.Name, name, StringComparison.Ordinal)); 132 | } 133 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.RenderExtensions.Tests/InvocationDescriptionExtensionsTests.MatchesParameters.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.RenderExtensions.Tests; 2 | 3 | [TestClass] 4 | public partial class InvocationDescriptionExtensionsTests 5 | { 6 | [TestMethod] 7 | public void MatchesParameters_NullMethod_Should_Throw() 8 | { 9 | // Assign 10 | var invocation = new InvocationDescription("System.Object", "Method"); 11 | 12 | // Act 13 | Action action = () => invocation.MatchesParameters(default); 14 | 15 | // Assert 16 | action.Should().Throw() 17 | .And.ParamName.Should().Be("method"); 18 | } 19 | 20 | [TestMethod] 21 | public void MatchesParameters_NullInvocation_Should_Throw() 22 | { 23 | // Assign 24 | var method = new MethodDescription("void", "Method"); 25 | 26 | // Act 27 | Action action = () => ((InvocationDescription)default).MatchesParameters(method); 28 | 29 | // Assert 30 | action.Should().Throw() 31 | .And.ParamName.Should().Be("invocation"); 32 | } 33 | 34 | [TestMethod] 35 | public void MatchesParameters_InvocationWithoutArguments_And_MethodWithoutParameters_Should_Match() 36 | { 37 | // Assign 38 | var method = new MethodDescription("void", "Method"); 39 | 40 | var invocation = new InvocationDescription("System.Object", "Method"); 41 | 42 | // Act 43 | var result = invocation.MatchesParameters(method); 44 | 45 | // Assert 46 | result.Should().BeTrue(); 47 | } 48 | 49 | [TestMethod] 50 | public void MatchesParameters_InvocationWithNoArguments_And_MethodWithParameters_Should_NotMatch() 51 | { 52 | // Assign 53 | var method = new MethodDescription("void", "Method"); 54 | method.Parameters.Add(new ParameterDescription("string", "parameter1")); 55 | 56 | var invocation = new InvocationDescription("System.Object", "Method"); 57 | 58 | // Act 59 | var result = invocation.MatchesParameters(method); 60 | 61 | // Assert 62 | result.Should().BeFalse(); 63 | } 64 | 65 | [TestMethod] 66 | public void MatchesParameters_InvocationWithArguments_And_MethodWithMoreParameters_Should_NotMatch() 67 | { 68 | // Assign 69 | var method = new MethodDescription("void", "Method"); 70 | method.Parameters.Add(new ParameterDescription("string", "parameter1")); 71 | method.Parameters.Add(new ParameterDescription("string", "parameter2")); 72 | 73 | var invocation = new InvocationDescription("System.Object", "Method"); 74 | invocation.Arguments.Add(new ArgumentDescription("string", "attribute1")); 75 | 76 | // Act 77 | var result = invocation.MatchesParameters(method); 78 | 79 | // Assert 80 | result.Should().BeFalse(); 81 | } 82 | 83 | [TestMethod] 84 | public void MatchesParameters_InvocationWithArguments_And_MethodWithLessParameters_Should_NotMatch() 85 | { 86 | // Assign 87 | var method = new MethodDescription("void", "Method"); 88 | method.Parameters.Add(new ParameterDescription("string", "parameter1")); 89 | 90 | var invocation = new InvocationDescription("System.Object", "Method"); 91 | invocation.Arguments.Add(new ArgumentDescription("string", "attribute1")); 92 | invocation.Arguments.Add(new ArgumentDescription("string", "attribute2")); 93 | 94 | // Act 95 | var result = invocation.MatchesParameters(method); 96 | 97 | // Assert 98 | result.Should().BeFalse(); 99 | } 100 | 101 | [TestMethod] 102 | public void MatchesParameters_InvocationWithSameArguments_And_MethodParameters_Should_Match() 103 | { 104 | // Assign 105 | var method = new MethodDescription("void", "Method"); 106 | method.Parameters.Add(new ParameterDescription("string", "parameter1")); 107 | 108 | var invocation = new InvocationDescription("System.Object", "Method"); 109 | invocation.Arguments.Add(new ArgumentDescription("string", "attribute1")); 110 | 111 | // Act 112 | var result = invocation.MatchesParameters(method); 113 | 114 | // Assert 115 | result.Should().BeTrue(); 116 | } 117 | 118 | [TestMethod] 119 | public void MatchesParameters_InvocationWithDifferentArguments_And_MethodParameters_Should_NotMatch() 120 | { 121 | // Assign 122 | var method = new MethodDescription("void", "Method"); 123 | method.Parameters.Add(new ParameterDescription("string", "parameter1")); 124 | 125 | var invocation = new InvocationDescription("System.Object", "Method"); 126 | invocation.Arguments.Add(new ArgumentDescription("int", "attribute1")); 127 | 128 | // Act 129 | var result = invocation.MatchesParameters(method); 130 | 131 | // Assert 132 | result.Should().BeFalse(); 133 | } 134 | 135 | [TestMethod] 136 | public void MatchesParameters_InvocationWithArguments_And_MethodWithMoreOptionalParameters_Should_Match() 137 | { 138 | // Assign 139 | var method = new MethodDescription("void", "Method"); 140 | method.Parameters.Add(new ParameterDescription("string", "parameter1")); 141 | method.Parameters.Add(new ParameterDescription("string", "parameter2") { HasDefaultValue = true }); 142 | 143 | var invocation = new InvocationDescription("System.Object", "Method"); 144 | invocation.Arguments.Add(new ArgumentDescription("string", "attribute1")); 145 | 146 | // Act 147 | var result = invocation.MatchesParameters(method); 148 | 149 | // Assert 150 | result.Should().BeTrue(); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /tests/LivingDocumentation.RenderExtensions.Tests/TypeDescriptionListExtensionsTests.PopulateInheritedBaseTypes.cs: -------------------------------------------------------------------------------- 1 | namespace LivingDocumentation.RenderExtensions.Tests; 2 | 3 | [TestClass] 4 | public partial class TypeDescriptionListExtensions 5 | { 6 | [TestMethod] 7 | public void PopulateInheritedBaseTypes_NullTypes_Should_Throw() 8 | { 9 | // Assign 10 | var types = (List)default; 11 | 12 | // Act 13 | Action action = () => types.PopulateInheritedBaseTypes(); 14 | 15 | // Assert 16 | action.Should().Throw() 17 | .And.ParamName.Should().Be("types"); 18 | } 19 | 20 | [TestMethod] 21 | public void PopulateInheritedBaseTypes_NoBaseTypes_Should_NotThrow() 22 | { 23 | // Assign 24 | var types = new[] { 25 | new TypeDescription(TypeType.Class, "Test") 26 | }; 27 | 28 | // Act 29 | Action action = () => types.PopulateInheritedBaseTypes(); 30 | 31 | // Assert 32 | action.Should().NotThrow(); 33 | } 34 | 35 | [TestMethod] 36 | public void PopulateInheritedBaseTypes_UnknownBaseTypes_Should_NotThrow() 37 | { 38 | // Assign 39 | var types = new[] { 40 | new TypeDescription(TypeType.Class, "Test") 41 | { 42 | BaseTypes = 43 | { 44 | "XXX" 45 | } 46 | }, 47 | }; 48 | 49 | // Act 50 | Action action = () => types.PopulateInheritedBaseTypes(); 51 | 52 | // Assert 53 | action.Should().NotThrow(); 54 | } 55 | 56 | [TestMethod] 57 | public void PopulateInheritedBaseTypes_BaseType_Should_BeCopiedToImplementingType() 58 | { 59 | // Assign 60 | var types = new[] { 61 | new TypeDescription(TypeType.Class, "Test") 62 | { 63 | BaseTypes = 64 | { 65 | "BaseTest" 66 | } 67 | }, 68 | new TypeDescription(TypeType.Class, "BaseTest") 69 | { 70 | BaseTypes = 71 | { 72 | "System.Object" 73 | } 74 | } 75 | }; 76 | 77 | // Act 78 | types.PopulateInheritedBaseTypes(); 79 | 80 | // Assert 81 | types[0].BaseTypes.Should().HaveCount(2); 82 | types[0].BaseTypes.Should().BeEquivalentTo("BaseTest", "System.Object"); 83 | } 84 | 85 | [TestMethod] 86 | public void PopulateInheritedBaseTypes_BaseType_Should_NotBeAltered() 87 | { 88 | // Assign 89 | var types = new[] { 90 | new TypeDescription(TypeType.Class, "Test") 91 | { 92 | BaseTypes = 93 | { 94 | "BaseTest" 95 | } 96 | }, 97 | new TypeDescription(TypeType.Class, "BaseTest") 98 | { 99 | BaseTypes = 100 | { 101 | "System.Object" 102 | } 103 | } 104 | }; 105 | 106 | // Act 107 | types.PopulateInheritedBaseTypes(); 108 | 109 | // Assert 110 | types[1].BaseTypes.Should().HaveCount(1); 111 | types[1].BaseTypes.Should().BeEquivalentTo("System.Object"); 112 | } 113 | 114 | [TestMethod] 115 | public void PopulateInheritedBaseTypes_BaseTypes_Should_BeCopiedToAllInheritingLevels() 116 | { 117 | // Assign 118 | var types = new[] { 119 | new TypeDescription(TypeType.Class, "Test") 120 | { 121 | BaseTypes = 122 | { 123 | "BaseTest" 124 | } 125 | }, 126 | new TypeDescription(TypeType.Class, "BaseTest") 127 | { 128 | BaseTypes = 129 | { 130 | "BaserTest" 131 | } 132 | }, 133 | new TypeDescription(TypeType.Class, "BaserTest") 134 | { 135 | BaseTypes = 136 | { 137 | "System.Object" 138 | } 139 | } 140 | }; 141 | 142 | // Act 143 | types.PopulateInheritedBaseTypes(); 144 | 145 | // Assert 146 | types[0].BaseTypes.Should().HaveCount(3); 147 | types[0].BaseTypes.Should().BeEquivalentTo("BaseTest", "BaserTest", "System.Object"); 148 | 149 | types[1].BaseTypes.Should().HaveCount(2); 150 | types[1].BaseTypes.Should().BeEquivalentTo("BaserTest", "System.Object"); 151 | 152 | types[2].BaseTypes.Should().HaveCount(1); 153 | types[2].BaseTypes.Should().BeEquivalentTo("System.Object"); 154 | } 155 | 156 | [TestMethod] 157 | public void PopulateInheritedBaseTypes_BaseTypes_Should_NotBeDupplicated() 158 | { 159 | // Assign 160 | var types = new[] { 161 | new TypeDescription(TypeType.Class, "Test") 162 | { 163 | BaseTypes = 164 | { 165 | "BaseTest", 166 | "System.Object" 167 | } 168 | }, 169 | new TypeDescription(TypeType.Class, "BaseTest") 170 | { 171 | BaseTypes = 172 | { 173 | "System.Object" 174 | } 175 | } 176 | }; 177 | 178 | // Act 179 | types.PopulateInheritedBaseTypes(); 180 | 181 | // Assert 182 | types[0].BaseTypes.Should().HaveCount(2); 183 | types[0].BaseTypes.Should().BeEquivalentTo("BaseTest", "System.Object"); 184 | 185 | types[1].BaseTypes.Should().HaveCount(1); 186 | types[1].BaseTypes.Should().BeEquivalentTo("System.Object"); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | .vscode/ 10 | 11 | # User-specific files (MonoDevelop/Xamarin Studio) 12 | *.userprefs 13 | 14 | # Build results 15 | [Dd]ebug/ 16 | [Dd]ebugPublic/ 17 | [Rr]elease/ 18 | [Rr]eleases/ 19 | x64/ 20 | x86/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | [Ll]og/ 25 | 26 | # Visual Studio 2015 cache/options directory 27 | .vs/ 28 | 29 | # Dockerfile projects folder for restore-packages script 30 | csproj-files/ 31 | 32 | # .js files created on build: 33 | src/Web/WebMVC/wwwroot/js/site* 34 | 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | **/wwwroot/lib/ 37 | !/wwwroot/lib/signalr 38 | !/wwwroot/lib/toastr 39 | 40 | # MSTest test Results 41 | [Tt]est[Rr]esult*/ 42 | [Bb]uild[Ll]og.* 43 | 44 | # NUNIT 45 | *.VisualState.xml 46 | TestResult.xml 47 | 48 | tests-results/ 49 | 50 | # Build Results of an ATL Project 51 | [Dd]ebugPS/ 52 | [Rr]eleasePS/ 53 | dlldata.c 54 | 55 | # DNX 56 | project.lock.json 57 | artifacts/ 58 | 59 | *_i.c 60 | *_p.c 61 | *_i.h 62 | *.ilk 63 | *.meta 64 | *.obj 65 | *.pch 66 | *.pdb 67 | *.pgc 68 | *.pgd 69 | *.rsp 70 | *.sbr 71 | *.tlb 72 | *.tli 73 | *.tlh 74 | *.tmp 75 | *.tmp_proj 76 | *.log 77 | *.vspscc 78 | *.vssscc 79 | .builds 80 | *.pidb 81 | *.svclog 82 | *.scc 83 | 84 | # Chutzpah Test files 85 | _Chutzpah* 86 | 87 | # Visual C++ cache files 88 | ipch/ 89 | *.aps 90 | *.ncb 91 | *.opendb 92 | *.opensdf 93 | *.sdf 94 | *.cachefile 95 | *.VC.db 96 | *.VC.VC.opendb 97 | 98 | # Visual Studio profiler 99 | *.psess 100 | *.vsp 101 | *.vspx 102 | *.sap 103 | 104 | # TFS 2012 Local Workspace 105 | $tf/ 106 | 107 | # Guidance Automation Toolkit 108 | *.gpState 109 | 110 | # ReSharper is a .NET coding add-in 111 | _ReSharper*/ 112 | *.[Rr]e[Ss]harper 113 | *.DotSettings.user 114 | 115 | # JustCode is a .NET coding add-in 116 | .JustCode 117 | 118 | # TeamCity is a build add-in 119 | _TeamCity* 120 | 121 | # DotCover is a Code Coverage Tool 122 | *.dotCover 123 | 124 | # NCrunch 125 | _NCrunch_* 126 | .*crunch*.local.xml 127 | nCrunchTemp_* 128 | 129 | # MightyMoose 130 | *.mm.* 131 | AutoTest.Net/ 132 | 133 | # Web workbench (sass) 134 | .sass-cache/ 135 | 136 | # Installshield output folder 137 | [Ee]xpress/ 138 | 139 | # DocProject is a documentation generator add-in 140 | DocProject/buildhelp/ 141 | DocProject/Help/*.HxT 142 | DocProject/Help/*.HxC 143 | DocProject/Help/*.hhc 144 | DocProject/Help/*.hhk 145 | DocProject/Help/*.hhp 146 | DocProject/Help/Html2 147 | DocProject/Help/html 148 | 149 | # Click-Once directory 150 | publish/ 151 | 152 | # Publish Web Output 153 | *.[Pp]ublish.xml 154 | *.azurePubxml 155 | # TODO: Comment the next line if you want to checkin your web deploy settings 156 | # but database connection strings (with potential passwords) will be unencrypted 157 | *.pubxml 158 | *.publishproj 159 | 160 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 161 | # checkin your Azure Web App publish settings, but sensitive information contained 162 | # in these scripts will be unencrypted 163 | PublishScripts/ 164 | 165 | # NuGet Packages 166 | *.nupkg 167 | # The packages folder can be ignored because of Package Restore 168 | **/packages/* 169 | # except build/, which is used as an MSBuild target. 170 | !**/packages/build/ 171 | # Uncomment if necessary however generally it will be regenerated when needed 172 | #!**/packages/repositories.config 173 | # NuGet v3's project.json files produces more ignoreable files 174 | *.nuget.props 175 | *.nuget.targets 176 | 177 | # Microsoft Azure Build Output 178 | csx/ 179 | *.build.csdef 180 | 181 | # Microsoft Azure Emulator 182 | ecf/ 183 | rcf/ 184 | 185 | # Windows Store app package directories and files 186 | AppPackages/ 187 | BundleArtifacts/ 188 | Package.StoreAssociation.xml 189 | _pkginfo.txt 190 | 191 | # Visual Studio cache files 192 | # files ending in .cache can be ignored 193 | *.[Cc]ache 194 | # but keep track of directories ending in .cache 195 | !*.[Cc]ache/ 196 | 197 | # Others 198 | ClientBin/ 199 | ~$* 200 | *~ 201 | *.dbmdl 202 | *.dbproj.schemaview 203 | *.publishsettings 204 | node_modules/ 205 | orleans.codegen.cs 206 | 207 | # Since there are multiple workflows, uncomment next line to ignore bower_components 208 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 209 | #bower_components/ 210 | 211 | # RIA/Silverlight projects 212 | Generated_Code/ 213 | 214 | # Backup & report files from converting an old project file 215 | # to a newer Visual Studio version. Backup files are not needed, 216 | # because we have git ;-) 217 | _UpgradeReport_Files/ 218 | Backup*/ 219 | UpgradeLog*.XML 220 | UpgradeLog*.htm 221 | 222 | # SQL Server files 223 | *.mdf 224 | *.ldf 225 | 226 | # Business Intelligence projects 227 | *.rdl.data 228 | *.bim.layout 229 | *.bim_*.settings 230 | 231 | # Microsoft Fakes 232 | FakesAssemblies/ 233 | 234 | # GhostDoc plugin setting file 235 | *.GhostDoc.xml 236 | 237 | # Node.js Tools for Visual Studio 238 | .ntvs_analysis.dat 239 | 240 | # Visual Studio 6 build log 241 | *.plg 242 | 243 | # Visual Studio 6 workspace options file 244 | *.opt 245 | 246 | # Visual Studio LightSwitch build output 247 | **/*.HTMLClient/GeneratedArtifacts 248 | **/*.DesktopClient/GeneratedArtifacts 249 | **/*.DesktopClient/ModelManifest.xml 250 | **/*.Server/GeneratedArtifacts 251 | **/*.Server/ModelManifest.xml 252 | _Pvt_Extensions 253 | 254 | # Paket dependency manager 255 | .paket/paket.exe 256 | paket-files/ 257 | 258 | # FAKE - F# Make 259 | .fake/ 260 | 261 | # JetBrains Rider 262 | .idea/ 263 | *.sln.iml 264 | pub/ 265 | 266 | **/launchSettings.json -------------------------------------------------------------------------------- /samples/LivingDocumentation.eShopOnContainers/eShopOnContainerExtensions.cs: -------------------------------------------------------------------------------- 1 | using PlantUml.Builder; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace LivingDocumentation.eShopOnContainers 6 | { 7 | public static class eShopOnContainersExtensions 8 | { 9 | private const string IntegrationEvent = "IntegrationEvent"; 10 | private const string DomainEvent = "DomainEvent"; 11 | private const string DomainEventHandler = "DomainEventHandler"; 12 | private const string Command = "Command"; 13 | private const string CommandHandler = "CommandHandler"; 14 | 15 | public static Color ArrowColor(this string name) 16 | { 17 | if (name.EndsWith(IntegrationEvent)) 18 | { 19 | return NamedColor.Green; 20 | } 21 | 22 | if (name.EndsWith(DomainEvent)) 23 | { 24 | return NamedColor.OrangeRed; 25 | } 26 | 27 | if (name.EndsWith(Command)) 28 | { 29 | return NamedColor.DodgerBlue; 30 | } 31 | 32 | return null; 33 | } 34 | 35 | public static string FormatForDiagram(this string name) 36 | { 37 | if (name.EndsWith(IntegrationEvent)) 38 | { 39 | return name[..^IntegrationEvent.Length]; 40 | } 41 | 42 | if (name.EndsWith(DomainEvent)) 43 | { 44 | return name[..^DomainEvent.Length]; 45 | } 46 | 47 | if (name.EndsWith(Command)) 48 | { 49 | return name[..^Command.Length]; 50 | } 51 | 52 | if (name.EndsWith(DomainEventHandler)) 53 | { 54 | return name[..^DomainEventHandler.Length] + "\\n//<>//"; 55 | } 56 | 57 | if (name.EndsWith(CommandHandler)) 58 | { 59 | return name[..^CommandHandler.Length] + "\\n//<>//"; 60 | } 61 | 62 | return name; 63 | } 64 | 65 | public static bool IsCommandHandler(this TypeDescription type) 66 | { 67 | return !type.FullName.Contains(".IdentifiedCommandHandler") && type.GetCommandHandlerDeclaration() != null; 68 | } 69 | 70 | public static bool IsDomainEventHandler(this TypeDescription type) 71 | { 72 | return type.GetDomainEventHandlerDeclaration() != null; 73 | } 74 | 75 | public static bool IsCommand(this TypeDescription type) 76 | { 77 | return type.ImplementsTypeStartsWith("MediatR.IRequest<"); 78 | } 79 | 80 | public static bool IsDomainEvent(this TypeDescription type) 81 | { 82 | return type.ImplementsType("MediatR.INotification"); 83 | } 84 | 85 | public static bool IsIntegrationEvent(this TypeDescription type) 86 | { 87 | return type.ImplementsType("Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events.IntegrationEvent"); 88 | } 89 | 90 | public static string GetCommandHandlerDeclaration(this TypeDescription type) 91 | { 92 | return type.BaseTypes.FirstOrDefault(bt => bt.StartsWith("MediatR.IRequestHandler", StringComparison.Ordinal)); 93 | } 94 | 95 | public static string GetDomainEventHandlerDeclaration(this TypeDescription type) 96 | { 97 | return type.BaseTypes.FirstOrDefault(bt => bt.StartsWith("MediatR.INotificationHandler", StringComparison.Ordinal)); 98 | } 99 | 100 | public static MethodDescription HandlingMethod(this TypeDescription type, string messageType) 101 | { 102 | return type.Methods.FirstOrDefault(m => string.Equals(m.Name, "Handle") && string.Equals(m.Parameters.First().Type, messageType)); 103 | } 104 | 105 | public static bool IsDomainEventCreation(this InvocationDescription invocation) 106 | { 107 | if (string.Equals(invocation.ContainingType, "Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork.Entity") 108 | && string.Equals(invocation.Name, "AddDomainEvent")) 109 | { 110 | return true; 111 | } 112 | 113 | if (string.Equals(invocation.ContainingType, "Ordering.API.Application.IntegrationEvents.IOrderingIntegrationEventService") 114 | && string.Equals(invocation.Name, "AddAndSaveEventAsync")) 115 | { 116 | return true; 117 | } 118 | 119 | return false; 120 | } 121 | 122 | public static bool IsAggregateRoot(this TypeDescription type) 123 | { 124 | return type != null 125 | && type.IsClass() 126 | && type.ImplementsType("Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork.IAggregateRoot"); 127 | } 128 | 129 | public static bool IsEnumeration(this TypeDescription type) 130 | { 131 | return type != null 132 | && type.IsClass() 133 | && type.ImplementsType("Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork.Enumeration"); 134 | } 135 | 136 | public static bool IsValueObject(this TypeDescription type) 137 | { 138 | return type != null 139 | && type.IsClass() 140 | && type.ImplementsType("Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork.ValueObject"); 141 | } 142 | 143 | public static bool IsEntity(this TypeDescription type) 144 | { 145 | return type != null 146 | && !type.IsAggregateRoot() 147 | && type.IsClass() 148 | && type.ImplementsType("Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork.Entity"); 149 | } 150 | } 151 | } 152 | --------------------------------------------------------------------------------