diff --git a/docs/DocDeprecation.md b/docs/DocDeprecation.md new file mode 100644 index 0000000..d9cfcd4 --- /dev/null +++ b/docs/DocDeprecation.md @@ -0,0 +1,22 @@ +# Summary.DocDeprecation +```cs +public record DocDeprecation +``` + +Contains deprecation information (e.g. the warning message). + +## Properties +### Message +```cs +public string? Message { get; init; } +``` + +The deprecation warning message. + +### Error +```cs +public bool Error { get; init; } +``` + +Whether the usage should be treated as an error instead of a warning. + diff --git a/docs/DocMember.md b/docs/DocMember.md index ed00a29..26fe741 100644 --- a/docs/DocMember.md +++ b/docs/DocMember.md @@ -49,6 +49,21 @@ public required DocType? DeclaringType { get; init; } The type that this member is declared in (works for nested types as well). +### Deprecated +```cs +[MemberNotNullWhen(true, nameof(Deprecation))] +public bool Deprecated { get; } +``` + +Whether the member is deprecated (e.g. marked with `[Obsolete]`). + +### Deprecation +```cs +public DocDeprecation? Deprecation { get; init; } +``` + +The member deprecation information. + ## Methods ### MatchesCref(string) ```cs diff --git a/docs/Sample{T0,T1}.Child.md b/docs/Sample{T0,T1}.Child.md index 3bdf914..a0965bc 100644 --- a/docs/Sample{T0,T1}.Child.md +++ b/docs/Sample{T0,T1}.Child.md @@ -1,5 +1,9 @@ -# Summary.Samples.Sample.Child +# ~~Summary.Samples.Sample.Child~~ +> [!WARNING] +> The type is deprecated. + ```cs +[Obsolete(error: true, message: "The type is deprecated.")] public class Child ``` diff --git a/docs/Sample{T0,T1}.md b/docs/Sample{T0,T1}.md index e7ba9c7..40f253c 100644 --- a/docs/Sample{T0,T1}.md +++ b/docs/Sample{T0,T1}.md @@ -52,8 +52,12 @@ public int Field A field of the child class. -### Field1 +### ~~Field1~~ +> [!WARNING] +> The field is deprecated. + ```cs +[Obsolete("The field is deprecated.")] public int Field1 ``` diff --git a/src/Core/DocDeprecation.cs b/src/Core/DocDeprecation.cs new file mode 100644 index 0000000..ec2465d --- /dev/null +++ b/src/Core/DocDeprecation.cs @@ -0,0 +1,17 @@ +namespace Summary; + +/// +/// Contains deprecation information (e.g. the warning message). +/// +public record DocDeprecation +{ + /// + /// The deprecation warning message. + /// + public string? Message { get; init; } + + /// + /// Whether the usage should be treated as an error instead of a warning. + /// + public bool Error { get; init; } +} \ No newline at end of file diff --git a/src/Core/DocMember.cs b/src/Core/DocMember.cs index 03d3948..4780d01 100644 --- a/src/Core/DocMember.cs +++ b/src/Core/DocMember.cs @@ -1,4 +1,6 @@ -namespace Summary; +using System.Diagnostics.CodeAnalysis; + +namespace Summary; /// /// A member of the generated document (e.g. type, field, property, method, etc.). @@ -36,6 +38,17 @@ public abstract record DocMember /// public required DocType? DeclaringType { get; init; } + /// + /// Whether the member is deprecated (e.g. marked with [Obsolete]). + /// + [MemberNotNullWhen(true, nameof(Deprecation))] + public bool Deprecated => Deprecation is not null; + + /// + /// The member deprecation information. + /// + public DocDeprecation? Deprecation { get; init; } + /// /// Whether this member matches the specified `cref` reference. /// diff --git a/src/Core/Extensions/StringExtensions.cs b/src/Core/Extensions/StringExtensions.cs index ef3c7ba..e183244 100644 --- a/src/Core/Extensions/StringExtensions.cs +++ b/src/Core/Extensions/StringExtensions.cs @@ -16,6 +16,13 @@ public static string Space(this string self) => public static string Surround(this string self, string left, string right) => self.Map(x => $"{left}{x}{right}"); + /// + /// Surrounds the specified string with the given prefix and suffix strings + /// if the specified condition is satisfied. If the string is empty, returns the string as is. + /// + public static string Surround(this string self, string left, string right, bool when) => + when ? self.Surround(left, right) : self; + private static string Map(this string self, Func map) => self is "" ? self : map(self); diff --git a/src/Core/Samples/Sample.cs b/src/Core/Samples/Sample.cs index b56440b..829ad98 100644 --- a/src/Core/Samples/Sample.cs +++ b/src/Core/Samples/Sample.cs @@ -29,6 +29,7 @@ public class Sample /// /// A child of the class. /// + [Obsolete(error: true, message: "The type is deprecated.")] public class Child { /// @@ -45,6 +46,7 @@ public class Child /// /// A sample field. /// + [Obsolete("The field is deprecated.")] public int Field1; /// diff --git a/src/Plugins/Markdown/MdRenderer.cs b/src/Plugins/Markdown/MdRenderer.cs index fde92fb..c6da41c 100644 --- a/src/Plugins/Markdown/MdRenderer.cs +++ b/src/Plugins/Markdown/MdRenderer.cs @@ -28,15 +28,15 @@ public void Dispose() => _renderer._level -= 2; } - private readonly StringBuilder _sb = new(); + private readonly StringBuilder _builder = new(); private int _level = 1; /// - /// Converts the rendered text into a string. + /// Converts the rendered Markdown into a string. /// public string Text() => - _sb.ToString(); + _builder.ToString(); /// /// Renders the specified documentation member into Markdown format. @@ -45,146 +45,177 @@ public string Text() => private MdRenderer Member(DocTypeDeclaration? parent, DocMember member) => member switch { - DocTypeDeclaration type => - MemberHeader(type).TypeDeclaration(type), - DocMethod method => - MemberHeader(method).Method(method), - DocProperty { Generated: true } generated => - GeneratedProperty(parent!, generated), - DocIndexer indexer => - MemberHeader(indexer).Indexer(indexer), - _ => - MemberHeader(member), + DocTypeDeclaration type => TypeDeclaration(type), + DocMethod method => Method(method), + DocProperty property => Property(parent!, property), + _ => Header(member), }; - private MdRenderer TypeDeclaration(DocTypeDeclaration type) => TypeParamsSection(type) - .MembersSection("Delegates", type, x => x.Delegate) - .MembersSection("Events", type, x => x.Event) - .MembersSection("Fields", type) - .MembersSection("Properties", type, x => !x.Event && x is not DocIndexer) - .MembersSection("Indexers", type) - .MembersSection("Methods", type, x => !x.Delegate); + private MdRenderer TypeDeclaration(DocTypeDeclaration type) => this + .Header(type) + .TypeParams(type) + .Members("Delegates", type, x => x.Delegate) + .Members("Events", type, x => x.Event) + .Members("Fields", type) + .Members("Properties", type, x => !x.Event && x is not DocIndexer) + .Members("Indexers", type) + .Members("Methods", type, x => !x.Delegate); + + private MdRenderer Method(DocMethod method) => this + .Header(method) + .TypeParams(method) + .Params(method) + .Returns(method.Comment); + + private MdRenderer Property(DocTypeDeclaration parent, DocProperty property) + { + if (property.Generated) + return this + .Name(property) + .Declaration(property) + .Element(parent.Comment.Param(property.Name)); - private MdRenderer Method(DocMethod method) => TypeParamsSection(method) - .ParamsSection(method) - .ReturnsSection(method.Comment); + if (property is DocIndexer indexer) + return Indexer(indexer); - private MdRenderer Indexer(DocIndexer indexer) => ParamsSection(indexer) - .ReturnsSection(indexer.Comment); + return Header(property); + } - private MdRenderer MemberHeader(DocMember member) => Name(member) + private MdRenderer Header(DocMember member) => this + .Name(member) + .Deprecation(member.Deprecation) .Declaration(member) .Element(member.Comment.Element("summary")) .Element(member.Comment.Element("remarks"), x => $"_{x}_") - .ElementSection("Example", member.Comment.Element("example")); + .Elements("Example", member.Comment.Element("example")); - private MdRenderer GeneratedProperty(DocTypeDeclaration parent, DocMember prop) => Name(prop) - .Declaration(prop) - .Element(parent.Comment.Param(prop.Name)); + private MdRenderer Indexer(DocIndexer indexer) => this + .Header(indexer) + .Params(indexer) + .Returns(indexer.Comment); // TODO: We can omit rendering `Name` and render `Declaration` only but it'd be nice to make this customizable via plugins. private MdRenderer Name(DocMember member) => member switch { - DocMethod x => + DocMethod method => Line($"{new string(c: '#', _level)} " + - $"{x.Name}" + - $"{x.TypeParams.Select(x => x.Name).Separated(", ").Surround("<", ">")}" + - $"({x.Params.Select(x => x.Type?.FullName).NonNulls().Separated(", ")})"), - DocIndexer x => - Line($"{new string(c: '#', _level)} this[{x.Params.Select(x => x.Type?.Name).NonNulls().Separated(", ")}]"), - DocTypeDeclaration x when _level is 1 => - Line($"{new string(c: '#', _level)} {member.FullyQualifiedName}{x.TypeParams.Select(x => x.Name).Separated(with: ", ").Surround("<", ">")}"), + $"{method.Name.Surround("~~", "~~", when: method.Deprecated)}" + + $"{method.TypeParams.Select(x => x.Name).Separated(", ").Surround("<", ">")}" + + $"({method.Params.Select(x => x.Type?.FullName).NonNulls().Separated(", ")})"), + DocIndexer indexer => + Line($"{new string(c: '#', _level)} this[{indexer.Params.Select(x => x.Type?.Name).NonNulls().Separated(", ")}]"), + DocTypeDeclaration type when _level is 1 => + Line($"{new string(c: '#', _level)} {type.FullyQualifiedName.Surround("~~", "~~", when: type.Deprecated)}{type.TypeParams.Select(x => x.Name).Separated(with: ", ").Surround("<", ">")}"), _ => - Line($"{new string(c: '#', _level)} {member.Name}"), + Line($"{new string(c: '#', _level)} {member.Name.Surround("~~", "~~", when: member.Deprecated)}"), }; - private MdRenderer ElementSection(string name, DocCommentElement? element, Func? map = null) => - element is null ? this : Line($"{new string(c: '#', _level + 1)} {name}").Element(element, map); + private MdRenderer Deprecation(DocDeprecation? deprecation) + { + if (!string.IsNullOrWhiteSpace(deprecation?.Message)) + { + // We currently render the deprecation message as an alert: https://github.com/orgs/community/discussions/16925. + // > Alerts are an extension of Markdown used to emphasize critical information. On GitHub, they + // > are displayed with distinctive colors and icons to indicate the importance of the content. + return this + .Line($"> [!WARNING]") + .Line($"> {deprecation.Message}") + .Line(); + } + + return this; + } - private MdRenderer Declaration(DocMember member) => Line("```cs") + private MdRenderer Declaration(DocMember member) => this + .Line("```cs") .Line(member.Declaration) .Line("```") .Line(); - private MdRenderer TypeParamsSection(DocMethod method) => - ParamsSection("Type Parameters", method.TypeParams.Select(x => (x.Name, x.Comment(method)))); + private MdRenderer TypeParams(DocMethod method) => + Params("Type Parameters", method.TypeParams.Select(x => (x.Name, x.Comment(method)))); - private MdRenderer TypeParamsSection(DocTypeDeclaration type) => - ParamsSection("Type Parameters", type.TypeParams.Select(x => (x.Name, x.Comment(type)))); + private MdRenderer TypeParams(DocTypeDeclaration type) => + Params("Type Parameters", type.TypeParams.Select(x => (x.Name, x.Comment(type)))); - private MdRenderer ParamsSection(DocMethod method) => - ParamsSection("Parameters", method.Params.Select(x => (x.Name, x.Comment(method)))); + private MdRenderer Params(DocMethod method) => + Params("Parameters", method.Params.Select(x => (x.Name, x.Comment(method)))); - private MdRenderer ParamsSection(DocIndexer method) => - ParamsSection("Parameters", method.Params.Select(x => (x.Name, x.Comment(method)))); + private MdRenderer Params(DocIndexer method) => + Params("Parameters", method.Params.Select(x => (x.Name, x.Comment(method)))); - private MdRenderer ParamsSection(string section, IEnumerable<(string Name, DocCommentElement? Comment)> parameters) - { - if (parameters.All(x => x.Comment is null)) - return this; + private MdRenderer Params(string section, IEnumerable<(string Name, DocCommentElement? Comment)> parameters) => + Params(section, parameters.Where(x => x.Comment is not null).ToList()!); - Section(section); + private MdRenderer Params(string section, ICollection<(string Name, DocCommentElement Comment)> parameters) => + Section(section, parameters, x => Line($"- `{x.Name}`: {x.Comment.Render()}")).Line(when: parameters.Any()); - foreach (var param in parameters) - Line($"- `{param.Name}`: {param.Comment.Render()}"); + private MdRenderer Members(string section, DocTypeDeclaration type) where T : DocMember => + Members(section, type, _ => true); - return Line(); - } + private MdRenderer Members(string section, DocTypeDeclaration type, Func p) where T : DocMember => + Members(section, type, type.Members.OfType().Where(p)); - private MdRenderer ReturnsSection(DocComment comment) => - ElementSection("Returns", comment.Element("returns")); + private MdRenderer Members(string section, DocTypeDeclaration type, IEnumerable members) where T : DocMember => + Section(section, members, x => Member(type, x)); - private MdRenderer MembersSection(string section, DocTypeDeclaration type) where T : DocMember => - MembersSection(section, type, _ => true); + private MdRenderer Returns(DocComment comment) => + Elements("Returns", comment.Element("returns")); - private MdRenderer MembersSection(string section, DocTypeDeclaration type, Func p) where T : DocMember => - MembersSection(section, type, type.Members.OfType().Where(p)); + private MdRenderer Elements(string section, DocCommentElement? element, Func? map = null) => + element is null ? this : Section(section).Element(element, map); - private MdRenderer MembersSection(string section, DocTypeDeclaration type, IEnumerable members) where T : DocMember => - MembersSection(section, type, members, Member); - - private MdRenderer MembersSection( - string section, - DocTypeDeclaration type, - IEnumerable members, - Func render) where T : DocMember + private MdRenderer Element(DocCommentElement? element, Func? map = null) { - if (members.Any()) - { - Section(section); + if (element is null) + return this; - using var _ = new Scope(this); + var lines = element + .Render() + .Split(NewLine) + .Select(x => x is "" ? x : map?.Invoke(x) ?? x); - foreach (var x in members) - render(type, x); - } + foreach (var line in lines) + Line(line); + + return Line(); + } + private MdRenderer Section(string name) + { + _builder.Append('#', _level + 1).Append(' ').AppendLine(name); return this; } - private MdRenderer Element(DocCommentElement? element, Func? map = null) + private MdRenderer Section(string name, IEnumerable items, Action render) { - if (element is null) - return this; + var scope = null as IDisposable; - var lines = - element - .Render() - .Split(NewLine) - .Select(x => x is "" ? x : map?.Invoke(x) ?? x); + try + { + foreach (var item in items) + { + scope ??= Section(name).Scoped(); - foreach (var line in lines) - Line(line); + render(item); + } + } + finally + { + scope?.Dispose(); + } - return Line(); + return this; } - private MdRenderer Section(string name) => - Line($"{new string(c: '#', _level + 1)} {name}"); + private MdRenderer Line(string text = "", bool when = true) + { + if (when) + _builder.AppendLine(text); - private MdRenderer Line(string s = "", bool when = true) => - With(when ? _sb.AppendLine(s) : _sb); + return this; + } - private MdRenderer With(T _) => - this; + private Scope Scoped() => + new(this); } \ No newline at end of file diff --git a/src/Plugins/Markdown/RenderMarkdownPipe.cs b/src/Plugins/Markdown/RenderMarkdownPipe.cs index d22c84d..a480447 100644 --- a/src/Plugins/Markdown/RenderMarkdownPipe.cs +++ b/src/Plugins/Markdown/RenderMarkdownPipe.cs @@ -10,12 +10,12 @@ public class RenderMarkdownPipe : IPipe { public Task Run(Doc doc) => Task.FromResult(doc - .Members - .Concat(doc .Members - .OfType() - .SelectMany(x => x.AllMembers.OfType())) - .Select(Render).ToArray()); + .Concat(doc + .Members + .OfType() + .SelectMany(x => x.AllMembers.OfType())) + .Select(Render).ToArray()); private static Md Render(DocMember member) { @@ -25,7 +25,7 @@ private static Md Render(DocMember member) string Name(DocMember x) => x switch { - DocTypeDeclaration { DeclaringType: null } type => $"{x.Name}{TypeParams(type)}", + DocTypeDeclaration { DeclaringType: null } type => $"{x.Name}{TypeParams(type)}", DocTypeDeclaration { DeclaringType: not null } type => $"{type.DeclaringType!.FullName.AsCref()}.{x.Name}{TypeParams(type)}", _ => x.Name, diff --git a/src/Plugins/Roslyn/CSharp/Extensions/FieldSyntaxExtensions.cs b/src/Plugins/Roslyn/CSharp/Extensions/FieldSyntaxExtensions.cs index 0743ad3..b64530d 100644 --- a/src/Plugins/Roslyn/CSharp/Extensions/FieldSyntaxExtensions.cs +++ b/src/Plugins/Roslyn/CSharp/Extensions/FieldSyntaxExtensions.cs @@ -32,5 +32,6 @@ private static DocField Field(this VariableDeclaratorSyntax self, FieldDeclarati Access = field.Access(), Comment = field.Comment(), DeclaringType = self.DeclaringType(), + Deprecation = field.AttributeLists.Deprecation(), }; } \ No newline at end of file diff --git a/src/Plugins/Roslyn/CSharp/Extensions/MethodSyntaxExtensions.cs b/src/Plugins/Roslyn/CSharp/Extensions/MethodSyntaxExtensions.cs index 13beee8..30a200e 100644 --- a/src/Plugins/Roslyn/CSharp/Extensions/MethodSyntaxExtensions.cs +++ b/src/Plugins/Roslyn/CSharp/Extensions/MethodSyntaxExtensions.cs @@ -20,6 +20,7 @@ public static DocMethod Method(this MethodDeclarationSyntax self) => Access = self.Access(), Comment = self.Comment(), DeclaringType = self.DeclaringType(), + Deprecation = self.AttributeLists.Deprecation(), Params = self.ParameterList.Params(), TypeParams = self.TypeParameterList.TypeParams(), Delegate = false, @@ -39,6 +40,7 @@ public static DocMethod Method(this MethodDeclarationSyntax self) => Params = self.ParameterList.Params(), TypeParams = self.TypeParameterList.TypeParams(), DeclaringType = self.DeclaringType(), + Deprecation = self.AttributeLists.Deprecation(), Delegate = true, }; } \ No newline at end of file diff --git a/src/Plugins/Roslyn/CSharp/Extensions/PropertySyntaxExtensions.cs b/src/Plugins/Roslyn/CSharp/Extensions/PropertySyntaxExtensions.cs index 57fef30..588a94e 100644 --- a/src/Plugins/Roslyn/CSharp/Extensions/PropertySyntaxExtensions.cs +++ b/src/Plugins/Roslyn/CSharp/Extensions/PropertySyntaxExtensions.cs @@ -27,6 +27,7 @@ public static DocProperty Property(this PropertyDeclarationSyntax self) => Access = self.Access(), Comment = self.Comment(), DeclaringType = self.DeclaringType(), + Deprecation = self.AttributeLists.Deprecation(), Generated = false, Event = false, }; @@ -47,6 +48,7 @@ public static DocProperty Property(this ParameterSyntax self) => Access = AccessModifier.Public, Comment = DocComment.Empty, DeclaringType = self.DeclaringType(), + Deprecation = self.AttributeLists.Deprecation(), Generated = true, Event = false, }; @@ -64,6 +66,7 @@ public static DocProperty Property(this EventDeclarationSyntax self) => Access = self.Access(), Comment = self.Comment(), DeclaringType = self.DeclaringType(), + Deprecation = self.AttributeLists.Deprecation(), Generated = false, Event = true, }; @@ -90,6 +93,7 @@ public static DocIndexer Indexer(this IndexerDeclarationSyntax self) => Access = self.Access(), Comment = self.Comment(), DeclaringType = self.DeclaringType(), + Deprecation = self.AttributeLists.Deprecation(), Generated = false, Event = false, Params = self.ParameterList.Params(), @@ -105,6 +109,7 @@ private static DocProperty Property(this VariableDeclaratorSyntax self, EventFie Access = field.Access(), Comment = field.Comment(), DeclaringType = self.DeclaringType(), + Deprecation = field.AttributeLists.Deprecation(), Generated = false, Event = true, }; diff --git a/src/Plugins/Roslyn/CSharp/Extensions/SyntaxExtensions.cs b/src/Plugins/Roslyn/CSharp/Extensions/SyntaxExtensions.cs index 6152a1f..f2bbae0 100644 --- a/src/Plugins/Roslyn/CSharp/Extensions/SyntaxExtensions.cs +++ b/src/Plugins/Roslyn/CSharp/Extensions/SyntaxExtensions.cs @@ -81,6 +81,59 @@ public static AccessModifier Access(this MemberDeclarationSyntax self) public static DocType? DeclaringType(this SyntaxNode self) => self.Ancestors().OfType().FirstOrDefault()?.Type(); + /// + /// A that contains the member deprecation information. + /// + public static DocDeprecation? Deprecation(this SyntaxList self) + { + var attribute = self + .SelectMany(x => x.Attributes) + .FirstOrDefault(x => x.Name.ToString() is + "Obsolete" or "System.Obsolete" or "global::System.Obsolete" or + "ObsoleteAttribute" or "System.ObsoleteAttribute" or "global::System.ObsoleteAttribute"); + + if (attribute is null) + return null; + + return new DocDeprecation + { + Message = Message(attribute), + Error = Error(attribute), + }; + + static string? Message(AttributeSyntax attribute) + { + var message = Argument(attribute, position: 0, name: "message")?.Expression.ToString(); + if (message is not null && message.StartsWith("\"") && message.EndsWith("\"")) + return message[1..^1]; + + return message; + } + + static bool Error(AttributeSyntax attribute) + { + var error = Argument(attribute, position: 1, name: "error")?.Expression.ToString(); + + return bool.TryParse(error, out var parsed) && parsed; + } + + static AttributeArgumentSyntax? Argument(AttributeSyntax attribute, int position, string name) + { + if (attribute.ArgumentList is null) + return null; + + if (attribute.ArgumentList.Arguments.Count > position) + { + var argument = attribute.ArgumentList.Arguments[position]; + if (argument is { NameColon: null, NameEquals: null }) + return argument; + } + + return attribute.ArgumentList.Arguments.FirstOrDefault( + x => x.NameColon?.Name.Identifier.ValueText == name); + } + } + /// /// A list of attributes of the specified member formatted as a string. /// diff --git a/src/Plugins/Roslyn/CSharp/Extensions/TypeDeclarationSyntaxExtensions.cs b/src/Plugins/Roslyn/CSharp/Extensions/TypeDeclarationSyntaxExtensions.cs index 83af0f9..f79804e 100644 --- a/src/Plugins/Roslyn/CSharp/Extensions/TypeDeclarationSyntaxExtensions.cs +++ b/src/Plugins/Roslyn/CSharp/Extensions/TypeDeclarationSyntaxExtensions.cs @@ -21,6 +21,7 @@ public static DocTypeDeclaration TypeDeclaration(this TypeDeclarationSyntax self Access = self.Access(), Comment = self.Comment(), DeclaringType = self.DeclaringType(), + Deprecation = self.AttributeLists.Deprecation(), Members = self.Members(), TypeParams = self.TypeParams(), Base = self.BaseList?.Types.Select(x => x.Type.Type()).ToArray() ?? Array.Empty(),