diff --git a/.editorconfig b/.editorconfig index b8c84e8..60e92e4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,6 +17,11 @@ charset = utf-8 # Generated code [*{_AssemblyInfo.cs,.notsupported.cs,AsmOffsets.cs}] generated_code = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion # C# files [*.cs] @@ -56,7 +61,7 @@ dotnet_style_predefined_type_for_member_access = true:suggestion # name all constant fields using PascalCase dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style dotnet_naming_symbols.constant_fields.applicable_kinds = field dotnet_naming_symbols.constant_fields.required_modifiers = const dotnet_naming_style.pascal_case_style.capitalization = pascal_case @@ -64,7 +69,7 @@ dotnet_naming_style.pascal_case_style.capitalization = pascal_case # static fields should have s_ prefix dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields -dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style dotnet_naming_symbols.static_fields.applicable_kinds = field dotnet_naming_symbols.static_fields.required_modifiers = static dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected @@ -74,7 +79,7 @@ dotnet_naming_style.static_prefix_style.capitalization = camel_case # internal and private fields should be _camelCase dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields -dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style dotnet_naming_symbols.private_internal_fields.applicable_kinds = field dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal dotnet_naming_style.camel_case_underscore_style.required_prefix = _ @@ -157,8 +162,19 @@ csharp_space_between_square_brackets = false dotnet_diagnostic.IDE0073.severity = error # License header file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion + +# IDE0060: Remove unused parameter +dotnet_diagnostic.IDE0060.severity = error # C++ Files + +# xUnit1006: Theory methods should have parameters +dotnet_diagnostic.xUnit1006.severity = error + [*.{cpp,h,in}] curly_bracket_next_line = true indent_brace_style = Allman diff --git a/.gitignore b/.gitignore index b97f381..dceaa05 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,10 @@ syntax: glob *.sln.docstates launchSettings.json +# Live Unit Tests +.lutignore +*.lutconfig + # Build results artifacts/ @@ -130,4 +134,4 @@ node_modules/ # Python Compile Outputs -*.pyc \ No newline at end of file +*.pyc diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..8ab6c26 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,5 @@ + + + false + + diff --git a/src/PortToTripleSlash/src/libraries/Configuration.cs b/src/PortToTripleSlash/src/libraries/Configuration.cs index 8383a03..f1b47e1 100644 --- a/src/PortToTripleSlash/src/libraries/Configuration.cs +++ b/src/PortToTripleSlash/src/libraries/Configuration.cs @@ -25,7 +25,8 @@ private enum Mode Initial, IsMono, SkipInterfaceImplementations, - SkipInterfaceRemarks + SkipInterfaceRemarks, + SkipRemarks } // The default boilerplate string for what dotnet-api-docs @@ -64,6 +65,7 @@ private enum Mode public bool IsMono { get; set; } public bool SkipInterfaceImplementations { get; set; } = false; public bool SkipInterfaceRemarks { get; set; } = true; + public bool SkipRemarks { get; set; } = true; public static Configuration GetCLIArguments(string[] args) { @@ -331,6 +333,10 @@ public static Configuration GetCLIArguments(string[] args) mode = Mode.SkipInterfaceRemarks; break; + case "-SKIPREMARKS": + mode = Mode.SkipRemarks; + break; + default: Log.ErrorAndExit($"Unrecognized argument: {arg}"); break; @@ -358,6 +364,13 @@ public static Configuration GetCLIArguments(string[] args) break; } + case Mode.SkipRemarks: + { + config.SkipRemarks = ParseOrExit(arg, nameof(Mode.SkipRemarks)); + mode = Mode.Initial; + break; + } + default: { Log.ErrorAndExit("Unexpected mode."); @@ -490,6 +503,10 @@ Whether you want interface implementation remarks to be used when the API itself the interface API. Usage example: -SkipInterfaceRemarks false + -SkipRemarks bool Default is true (excludes remarks). + Whether you want to backport remarks. + Usage example: + -SkipRemarks true "); Log.Warning(@" TL;DR: diff --git a/src/PortToTripleSlash/src/libraries/Docs/DocsAPI.cs b/src/PortToTripleSlash/src/libraries/Docs/DocsAPI.cs index b11d512..6c3ae9a 100644 --- a/src/PortToTripleSlash/src/libraries/Docs/DocsAPI.cs +++ b/src/PortToTripleSlash/src/libraries/Docs/DocsAPI.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -32,7 +32,6 @@ internal abstract class DocsAPI : IDocsAPI Params.Any(p => p.Value.IsDocsEmpty()) || TypeParams.Any(tp => tp.Value.IsDocsEmpty()); - public abstract bool Changed { get; set; } public string FilePath { get; set; } = string.Empty; public string DocId => _docId ??= GetApiSignatureDocId(); @@ -49,14 +48,7 @@ public List Parameters if (_parameters == null) { XElement? xeParameters = XERoot.Element("Parameters"); - if (xeParameters == null) - { - _parameters = new(); - } - else - { - _parameters = xeParameters.Elements("Parameter").Select(x => new DocsParameter(x)).ToList(); - } + _parameters = xeParameters == null ? (List)new() : xeParameters.Elements("Parameter").Select(x => new DocsParameter(x)).ToList(); } return _parameters; } @@ -72,220 +64,59 @@ public List TypeParameters if (_typeParameters == null) { XElement? xeTypeParameters = XERoot.Element("TypeParameters"); - if (xeTypeParameters == null) - { - _typeParameters = new(); - } - else - { - _typeParameters = xeTypeParameters.Elements("TypeParameter").Select(x => new DocsTypeParameter(x)).ToList(); - } + _typeParameters = xeTypeParameters == null ? (List)new() : xeTypeParameters.Elements("TypeParameter").Select(x => new DocsTypeParameter(x)).ToList(); } return _typeParameters; } } - public XElement Docs - { - get - { - return XERoot.Element("Docs") ?? throw new NullReferenceException($"Docs section was null in {FilePath}"); - } - } + public XElement Docs => XERoot.Element("Docs") ?? throw new NullReferenceException($"Docs section was null in {FilePath}"); /// /// The param elements found inside the Docs section. /// - public List Params - { - get - { - if (_params == null) - { - if (Docs != null) - { - _params = Docs.Elements("param").Select(x => new DocsParam(this, x)).ToList(); - } - else - { - _params = new List(); - } - } - return _params; - } - } + public List Params => _params ??= Docs != null ? Docs.Elements("param").Select(x => new DocsParam(this, x)).ToList() : new List(); /// /// The typeparam elements found inside the Docs section. /// - public List TypeParams - { - get - { - if (_typeParams == null) - { - if (Docs != null) - { - _typeParams = Docs.Elements("typeparam").Select(x => new DocsTypeParam(this, x)).ToList(); - } - else - { - _typeParams = new(); - } - } - return _typeParams; - } - } + public List TypeParams => _typeParams ??= Docs != null ? Docs.Elements("typeparam").Select(x => new DocsTypeParam(this, x)).ToList() : (List)new(); - public List SeeAlsoCrefs - { - get - { - if (_seeAlsoCrefs == null) - { - if (Docs != null) - { - _seeAlsoCrefs = Docs.Elements("seealso").Select(x => XmlHelper.GetAttributeValue(x, "cref").DocIdEscaped()).ToList(); - } - else - { - _seeAlsoCrefs = new(); - } - } - return _seeAlsoCrefs; - } - } + public List SeeAlsoCrefs => _seeAlsoCrefs ??= Docs != null ? Docs.Elements("seealso").Select(x => XmlHelper.GetAttributeValue(x, "cref").DocIdEscaped()).ToList() : (List)new(); - public List AltMembers - { - get - { - if (_altMemberCrefs == null) - { - if (Docs != null) - { - _altMemberCrefs = Docs.Elements("altmember").Select(x => XmlHelper.GetAttributeValue(x, "cref").DocIdEscaped()).ToList(); - } - else - { - _altMemberCrefs = new(); - } - } - return _altMemberCrefs; - } - } + public List AltMembers => _altMemberCrefs ??= Docs != null ? Docs.Elements("altmember").Select(x => XmlHelper.GetAttributeValue(x, "cref").DocIdEscaped()).ToList() : (List)new(); - public List Relateds - { - get - { - if (_relateds == null) - { - if (Docs != null) - { - _relateds = Docs.Elements("related").Select(x => new DocsRelated(this, x)).ToList(); - } - else - { - _relateds = new(); - } - } - return _relateds; - } - } + public List Relateds => _relateds ??= Docs != null ? Docs.Elements("related").Select(x => new DocsRelated(this, x)).ToList() : (List)new(); + + public abstract string Summary { get; } + + public abstract string Value { get; } - public abstract string Summary { get; set; } public abstract string ReturnType { get; } - public abstract string Returns { get; set; } - public abstract string Remarks { get; set; } + public abstract string Returns { get; } - public List AssemblyInfos - { - get - { - if (_assemblyInfos == null) - { - _assemblyInfos = new List(); - } - return _assemblyInfos; - } - } + public abstract string Remarks { get; } - public DocsParam SaveParam(XElement xeIntelliSenseXmlParam) - { - XElement xeDocsParam = new XElement(xeIntelliSenseXmlParam.Name); - xeDocsParam.ReplaceAttributes(xeIntelliSenseXmlParam.Attributes()); - XmlHelper.SaveFormattedAsXml(xeDocsParam, xeIntelliSenseXmlParam.Value); - DocsParam docsParam = new DocsParam(this, xeDocsParam); - Changed = true; - return docsParam; - } + public abstract List Exceptions { get; } - public APIKind Kind - { - get - { - return this switch - { - DocsMember _ => APIKind.Member, - DocsType _ => APIKind.Type, - _ => throw new ArgumentException("Unrecognized IDocsAPI object") - }; - } - } + public List AssemblyInfos => _assemblyInfos ??= new List(); - public DocsTypeParam AddTypeParam(string name, string value) + public APIKind Kind => this switch { - XElement typeParam = new XElement("typeparam"); - typeParam.SetAttributeValue("name", name); - XmlHelper.AddChildFormattedAsXml(Docs, typeParam, value); - Changed = true; - return new DocsTypeParam(this, typeParam); - } + DocsMember _ => APIKind.Member, + DocsType _ => APIKind.Type, + _ => throw new ArgumentException("Unrecognized IDocsAPI object") + }; // For Types, these elements are called TypeSignature. // For Members, these elements are called MemberSignature. protected abstract string GetApiSignatureDocId(); - protected string GetNodesInPlainText(string name) - { - if (TryGetElement(name, addIfMissing: false, out XElement? element)) - { - if (name == "remarks") - { - XElement? formatElement = element.Element("format"); - if (formatElement != null) - { - element = formatElement; - } - } - - return XmlHelper.GetNodesInPlainText(element); - } - return string.Empty; - } - - protected void SaveFormattedAsXml(string name, string value, bool addIfMissing) - { - if (TryGetElement(name, addIfMissing, out XElement? element)) - { - XmlHelper.SaveFormattedAsXml(element, value); - Changed = true; - } - } - - protected void SaveFormattedAsMarkdown(string name, string value, bool addIfMissing, bool isMember) - { - if (TryGetElement(name, addIfMissing, out XElement? element)) - { - XmlHelper.SaveFormattedAsMarkdown(element, value, isMember); - Changed = true; - } - } + protected string GetNodesInPlainText(string name) => TryGetElement(name, out XElement? element) ? XmlHelper.GetNodesInPlainText(name, element) : string.Empty; // Returns true if the element existed or had to be created with "To be added." as value. Returns false the element was not found and a new one was not created. - private bool TryGetElement(string name, bool addIfMissing, [NotNullWhen(returnValue: true)] out XElement? element) + private bool TryGetElement(string name, [NotNullWhen(returnValue: true)] out XElement? element) { element = null; @@ -296,12 +127,6 @@ private bool TryGetElement(string name, bool addIfMissing, [NotNullWhen(returnVa element = Docs.Element(name); - if (element == null && addIfMissing) - { - element = new XElement(name); - XmlHelper.AddChildFormattedAsXml(Docs, element, Configuration.ToBeAdded); - } - return element != null; } } diff --git a/src/PortToTripleSlash/src/libraries/Docs/DocsAssemblyInfo.cs b/src/PortToTripleSlash/src/libraries/Docs/DocsAssemblyInfo.cs index beb87e4..1ff3096 100644 --- a/src/PortToTripleSlash/src/libraries/Docs/DocsAssemblyInfo.cs +++ b/src/PortToTripleSlash/src/libraries/Docs/DocsAssemblyInfo.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; @@ -10,31 +10,13 @@ namespace ApiDocsSync.PortToTripleSlash.Docs internal class DocsAssemblyInfo { private readonly XElement XEAssemblyInfo; - public string AssemblyName - { - get - { - return XmlHelper.GetChildElementValue(XEAssemblyInfo, "AssemblyName"); - } - } + + public string AssemblyName => XmlHelper.GetChildElementValue(XEAssemblyInfo, "AssemblyName"); private List? _assemblyVersions; - public List AssemblyVersions - { - get - { - if (_assemblyVersions == null) - { - _assemblyVersions = XEAssemblyInfo.Elements("AssemblyVersion").Select(x => XmlHelper.GetNodesInPlainText(x)).ToList(); - } - return _assemblyVersions; - } - } + public List AssemblyVersions => _assemblyVersions ??= XEAssemblyInfo.Elements("AssemblyVersion").Select(x => XmlHelper.GetNodesInPlainText("AssemblyVersion", x)).ToList(); - public DocsAssemblyInfo(XElement xeAssemblyInfo) - { - XEAssemblyInfo = xeAssemblyInfo; - } + public DocsAssemblyInfo(XElement xeAssemblyInfo) => XEAssemblyInfo = xeAssemblyInfo; public override string ToString() => AssemblyName; } diff --git a/src/PortToTripleSlash/src/libraries/Docs/DocsAttribute.cs b/src/PortToTripleSlash/src/libraries/Docs/DocsAttribute.cs index ed1d821..8e9ed7a 100644 --- a/src/PortToTripleSlash/src/libraries/Docs/DocsAttribute.cs +++ b/src/PortToTripleSlash/src/libraries/Docs/DocsAttribute.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Xml.Linq; @@ -9,24 +9,10 @@ internal class DocsAttribute { private readonly XElement XEAttribute; - public string FrameworkAlternate - { - get - { - return XmlHelper.GetAttributeValue(XEAttribute, "FrameworkAlternate"); - } - } - public string AttributeName - { - get - { - return XmlHelper.GetChildElementValue(XEAttribute, "AttributeName"); - } - } + public string FrameworkAlternate => XmlHelper.GetAttributeValue(XEAttribute, "FrameworkAlternate"); - public DocsAttribute(XElement xeAttribute) - { - XEAttribute = xeAttribute; - } + public string AttributeName => XmlHelper.GetChildElementValue(XEAttribute, "AttributeName"); + + public DocsAttribute(XElement xeAttribute) => XEAttribute = xeAttribute; } } diff --git a/src/PortToTripleSlash/src/libraries/Docs/DocsCommentsContainer.cs b/src/PortToTripleSlash/src/libraries/Docs/DocsCommentsContainer.cs index d0c8bb9..aa27735 100644 --- a/src/PortToTripleSlash/src/libraries/Docs/DocsCommentsContainer.cs +++ b/src/PortToTripleSlash/src/libraries/Docs/DocsCommentsContainer.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -13,7 +13,7 @@ namespace ApiDocsSync.PortToTripleSlash.Docs { internal class DocsCommentsContainer { - private Configuration Config { get; set; } + internal Configuration Config { get; } public readonly Dictionary Types = new(); public readonly Dictionary Members = new(); diff --git a/src/PortToTripleSlash/src/libraries/Docs/DocsException.cs b/src/PortToTripleSlash/src/libraries/Docs/DocsException.cs index e656853..586a78d 100644 --- a/src/PortToTripleSlash/src/libraries/Docs/DocsException.cs +++ b/src/PortToTripleSlash/src/libraries/Docs/DocsException.cs @@ -1,8 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; using System.Xml.Linq; namespace ApiDocsSync.PortToTripleSlash.Docs @@ -11,30 +9,11 @@ internal class DocsException { private readonly XElement XEException; - public IDocsAPI ParentAPI - { - get; private set; - } + public IDocsAPI ParentAPI { get; } - public string Cref - { - get - { - return XmlHelper.GetAttributeValue(XEException, "cref").DocIdEscaped(); - } - } + public string Cref => XmlHelper.GetAttributeValue(XEException, "cref").DocIdEscaped(); - public string Value - { - get - { - return XmlHelper.GetNodesInPlainText(XEException); - } - private set - { - XmlHelper.SaveFormattedAsXml(XEException, value); - } - } + public string Value => XmlHelper.GetNodesInPlainText("exception", XEException); public string OriginalValue { get; private set; } @@ -45,62 +24,6 @@ public DocsException(IDocsAPI parentAPI, XElement xException) OriginalValue = Value; } - public void AppendException(string toAppend) - { - XmlHelper.AppendFormattedAsXml(XEException, $"\r\n\r\n-or-\r\n\r\n{toAppend}", removeUndesiredEndlines: false); - ParentAPI.Changed = true; - } - - public bool WordCountCollidesAboveThreshold(string intelliSenseXmlValue, int threshold) - { - Dictionary hashIntelliSenseXml = GetHash(intelliSenseXmlValue); - Dictionary hashDocs = GetHash(Value); - - int collisions = 0; - // Iterate all the words of the IntelliSense xml exception string - foreach (KeyValuePair word in hashIntelliSenseXml) - { - // Check if the existing Docs string contained that word - if (hashDocs.ContainsKey(word.Key)) - { - // If the total found in Docs is >= than the total found in IntelliSense xml - // then consider it a collision - if (hashDocs[word.Key] >= word.Value) - { - collisions++; - } - } - } - - // If the number of word collisions is above the threshold, it probably means - // that part of the original TS string was included in the Docs string - double collisionPercentage = (collisions * 100 / (double)hashIntelliSenseXml.Count); - return collisionPercentage >= threshold; - } - - public override string ToString() - { - return $"{Cref} - {Value}"; - } - - // Gets a dictionary with the count of each character found in the string. - private Dictionary GetHash(string value) - { - Dictionary hash = new Dictionary(); - string[] words = value.Split(new char[] { ' ', '\'', '"', '\r', '\n', '.', ',', ';', ':' }, StringSplitOptions.RemoveEmptyEntries); - - foreach (string word in words) - { - if (hash.ContainsKey(word)) - { - hash[word]++; - } - else - { - hash.Add(word, 1); - } - } - return hash; - } + public override string ToString() => $"{Cref} - {Value}"; } } diff --git a/src/PortToTripleSlash/src/libraries/Docs/DocsMember.cs b/src/PortToTripleSlash/src/libraries/Docs/DocsMember.cs index f9c3524..2221b1d 100644 --- a/src/PortToTripleSlash/src/libraries/Docs/DocsMember.cs +++ b/src/PortToTripleSlash/src/libraries/Docs/DocsMember.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -24,43 +24,11 @@ public DocsMember(string filePath, DocsType parentType, XElement xeMember) public DocsType ParentType { get; private set; } - public override bool Changed - { - get => ParentType.Changed; - set => ParentType.Changed |= value; - } + public string MemberName => _memberName ??= XmlHelper.GetAttributeValue(XERoot, "MemberName"); - public string MemberName - { - get - { - if (_memberName == null) - { - _memberName = XmlHelper.GetAttributeValue(XERoot, "MemberName"); - } - return _memberName; - } - } - - public List MemberSignatures - { - get - { - if (_memberSignatures == null) - { - _memberSignatures = XERoot.Elements("MemberSignature").Select(x => new DocsMemberSignature(x)).ToList(); - } - return _memberSignatures; - } - } + public List MemberSignatures => _memberSignatures ??= XERoot.Elements("MemberSignature").Select(x => new DocsMemberSignature(x)).ToList(); - public string MemberType - { - get - { - return XmlHelper.GetChildElementValue(XERoot, "MemberType"); - } - } + public string MemberType => XmlHelper.GetChildElementValue(XERoot, "MemberType"); public string ImplementsInterfaceMember { @@ -76,77 +44,19 @@ public override string ReturnType get { XElement? xeReturnValue = XERoot.Element("ReturnValue"); - if (xeReturnValue != null) - { - return XmlHelper.GetChildElementValue(xeReturnValue, "ReturnType"); - } - return string.Empty; + return xeReturnValue != null ? XmlHelper.GetChildElementValue(xeReturnValue, "ReturnType") : string.Empty; } } - public override string Returns - { - get - { - return (ReturnType != "System.Void") ? GetNodesInPlainText("returns") : string.Empty; - } - set - { - if (ReturnType != "System.Void") - { - SaveFormattedAsXml("returns", value, addIfMissing: false); - } - else - { - Log.Warning($"Attempted to save a returns item for a method that returns System.Void: {DocId}"); - } - } - } + public override string Returns => (ReturnType != "System.Void") ? GetNodesInPlainText("returns") : string.Empty; - public override string Summary - { - get - { - return GetNodesInPlainText("summary"); - } - set - { - SaveFormattedAsXml("summary", value, addIfMissing: true); - } - } + public override string Summary => GetNodesInPlainText("summary"); - public override string Remarks - { - get - { - return GetNodesInPlainText("remarks"); - } - set - { - SaveFormattedAsMarkdown("remarks", value, addIfMissing: !value.IsDocsEmpty(), isMember: true); - } - } + public override string Remarks => GetNodesInPlainText("remarks"); - public string Value - { - get - { - return (MemberType == "Property") ? GetNodesInPlainText("value") : string.Empty; - } - set - { - if (MemberType == "Property") - { - SaveFormattedAsXml("value", value, addIfMissing: true); - } - else - { - Log.Warning($"Attempted to save a value element for an API that is not a property: {DocId}"); - } - } - } + public override string Value => (MemberType == "Property") ? GetNodesInPlainText("value") : string.Empty; - public List Exceptions + public override List Exceptions { get { @@ -165,29 +75,12 @@ public List Exceptions } } - public override string ToString() - { - return DocId; - } - - public DocsException AddException(string cref, string value) - { - XElement exception = new XElement("exception"); - exception.SetAttributeValue("cref", cref); - XmlHelper.SaveFormattedAsXml(exception, value, removeUndesiredEndlines: false); - Docs.Add(exception); - Changed = true; - return new DocsException(this, exception); - } + public override string ToString() => DocId; protected override string GetApiSignatureDocId() { DocsMemberSignature? dts = MemberSignatures.FirstOrDefault(x => x.Language == "DocId"); - if (dts == null) - { - throw new FormatException($"DocId TypeSignature not found for {MemberName}"); - } - return dts.Value; + return dts != null ? dts.Value : throw new FormatException($"DocId TypeSignature not found for {MemberName}"); } } } diff --git a/src/PortToTripleSlash/src/libraries/Docs/DocsMemberSignature.cs b/src/PortToTripleSlash/src/libraries/Docs/DocsMemberSignature.cs index 0f97f29..8d9020f 100644 --- a/src/PortToTripleSlash/src/libraries/Docs/DocsMemberSignature.cs +++ b/src/PortToTripleSlash/src/libraries/Docs/DocsMemberSignature.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Xml.Linq; @@ -9,25 +9,10 @@ internal class DocsMemberSignature { private readonly XElement XEMemberSignature; - public string Language - { - get - { - return XmlHelper.GetAttributeValue(XEMemberSignature, "Language"); - } - } + public string Language => XmlHelper.GetAttributeValue(XEMemberSignature, "Language"); - public string Value - { - get - { - return XmlHelper.GetAttributeValue(XEMemberSignature, "Value"); - } - } + public string Value => XmlHelper.GetAttributeValue(XEMemberSignature, "Value"); - public DocsMemberSignature(XElement xeMemberSignature) - { - XEMemberSignature = xeMemberSignature; - } + public DocsMemberSignature(XElement xeMemberSignature) => XEMemberSignature = xeMemberSignature; } } diff --git a/src/PortToTripleSlash/src/libraries/Docs/DocsParam.cs b/src/PortToTripleSlash/src/libraries/Docs/DocsParam.cs index 0bde78e..39761b1 100644 --- a/src/PortToTripleSlash/src/libraries/Docs/DocsParam.cs +++ b/src/PortToTripleSlash/src/libraries/Docs/DocsParam.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Xml.Linq; @@ -8,37 +8,19 @@ namespace ApiDocsSync.PortToTripleSlash.Docs internal class DocsParam { private readonly XElement XEDocsParam; - public IDocsAPI ParentAPI - { - get; private set; - } - public string Name - { - get - { - return XmlHelper.GetAttributeValue(XEDocsParam, "name"); - } - } - public string Value - { - get - { - return XmlHelper.GetNodesInPlainText(XEDocsParam); - } - set - { - XmlHelper.SaveFormattedAsXml(XEDocsParam, value); - ParentAPI.Changed = true; - } - } + + public IDocsAPI ParentAPI { get; } + + public string Name => XmlHelper.GetAttributeValue(XEDocsParam, "name"); + + public string Value => XmlHelper.GetNodesInPlainText("param", XEDocsParam); + public DocsParam(IDocsAPI parentAPI, XElement xeDocsParam) { ParentAPI = parentAPI; XEDocsParam = xeDocsParam; } - public override string ToString() - { - return Name; - } + + public override string ToString() => Name; } } diff --git a/src/PortToTripleSlash/src/libraries/Docs/DocsParameter.cs b/src/PortToTripleSlash/src/libraries/Docs/DocsParameter.cs index 28a25a5..2eaa50d 100644 --- a/src/PortToTripleSlash/src/libraries/Docs/DocsParameter.cs +++ b/src/PortToTripleSlash/src/libraries/Docs/DocsParameter.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Xml.Linq; @@ -8,23 +8,11 @@ namespace ApiDocsSync.PortToTripleSlash.Docs internal class DocsParameter { private readonly XElement XEParameter; - public string Name - { - get - { - return XmlHelper.GetAttributeValue(XEParameter, "Name"); - } - } - public string Type - { - get - { - return XmlHelper.GetAttributeValue(XEParameter, "Type"); - } - } - public DocsParameter(XElement xeParameter) - { - XEParameter = xeParameter; - } + + public string Name => XmlHelper.GetAttributeValue(XEParameter, "Name"); + + public string Type => XmlHelper.GetAttributeValue(XEParameter, "Type"); + + public DocsParameter(XElement xeParameter) => XEParameter = xeParameter; } } diff --git a/src/PortToTripleSlash/src/libraries/Docs/DocsRelated.cs b/src/PortToTripleSlash/src/libraries/Docs/DocsRelated.cs index 7b63280..933dd2d 100644 --- a/src/PortToTripleSlash/src/libraries/Docs/DocsRelated.cs +++ b/src/PortToTripleSlash/src/libraries/Docs/DocsRelated.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Xml.Linq; @@ -9,24 +9,13 @@ internal class DocsRelated { private readonly XElement XERelatedArticle; - public IDocsAPI ParentAPI - { - get; private set; - } + public IDocsAPI ParentAPI { get; } public string ArticleType => XmlHelper.GetAttributeValue(XERelatedArticle, "type"); public string Href => XmlHelper.GetAttributeValue(XERelatedArticle, "href"); - public string Value - { - get => XmlHelper.GetNodesInPlainText(XERelatedArticle); - set - { - XmlHelper.SaveFormattedAsXml(XERelatedArticle, value); - ParentAPI.Changed = true; - } - } + public string Value => XmlHelper.GetNodesInPlainText("related", XERelatedArticle); public DocsRelated(IDocsAPI parentAPI, XElement xeRelatedArticle) { @@ -34,9 +23,6 @@ public DocsRelated(IDocsAPI parentAPI, XElement xeRelatedArticle) XERelatedArticle = xeRelatedArticle; } - public override string ToString() - { - return Value; - } + public override string ToString() => Value; } } diff --git a/src/PortToTripleSlash/src/libraries/Docs/DocsType.cs b/src/PortToTripleSlash/src/libraries/Docs/DocsType.cs index 8babb30..2002de9 100644 --- a/src/PortToTripleSlash/src/libraries/Docs/DocsType.cs +++ b/src/PortToTripleSlash/src/libraries/Docs/DocsType.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -33,13 +33,12 @@ public DocsType(string filePath, XDocument xDoc, XElement xeRoot, Encoding encod AssemblyInfos.AddRange(XERoot.Elements("AssemblyInfo").Select(x => new DocsAssemblyInfo(x))); } - public List? SymbolLocations { get; set; } + private List? _symbolLocations; + public List SymbolLocations => _symbolLocations ??= new(); - public XDocument XDoc { get; set; } + public XDocument XDoc { get; } - public override bool Changed { get; set; } - - public Encoding FileEncoding { get; internal set; } + public Encoding FileEncoding { get; } public string TypeName { @@ -64,29 +63,9 @@ public string TypeName } } - public string Name - { - get - { - if (_name == null) - { - _name = XmlHelper.GetAttributeValue(XERoot, "Name"); - } - return _name; - } - } + public string Name => _name ??= XmlHelper.GetAttributeValue(XERoot, "Name"); - public string FullName - { - get - { - if (_fullName == null) - { - _fullName = XmlHelper.GetAttributeValue(XERoot, "FullName"); - } - return _fullName; - } - } + public string FullName => _fullName ??= XmlHelper.GetAttributeValue(XERoot, "FullName"); public string Namespace { @@ -101,25 +80,9 @@ public string Namespace } } - public List TypeSignatures - { - get - { - if (_typesSignatures == null) - { - _typesSignatures = XERoot.Elements("TypeSignature").Select(x => new DocsTypeSignature(x)).ToList(); - } - return _typesSignatures; - } - } + public List TypeSignatures => _typesSignatures ??= XERoot.Elements("TypeSignature").Select(x => new DocsTypeSignature(x)).ToList(); - public XElement? Base - { - get - { - return XERoot.Element("Base"); - } - } + public XElement? Base => XERoot.Element("Base"); public string BaseTypeName { @@ -137,13 +100,7 @@ public string BaseTypeName } } - public XElement? Interfaces - { - get - { - return XERoot.Element("Interfaces"); - } - } + public XElement? Interfaces => XERoot.Element("Interfaces"); public List InterfaceNames { @@ -181,17 +138,9 @@ public List Attributes } } - public override string Summary - { - get - { - return GetNodesInPlainText("summary"); - } - set - { - SaveFormattedAsXml("summary", value, addIfMissing: true); - } - } + public override string Summary => GetNodesInPlainText("summary"); + + public override string Value => string.Empty; /// /// Only available when the type is a delegate. @@ -212,50 +161,18 @@ public override string ReturnType /// /// Only available when the type is a delegate. /// - public override string Returns - { - get - { - return (ReturnType != "System.Void") ? GetNodesInPlainText("returns") : string.Empty; - } - set - { - if (ReturnType != "System.Void") - { - SaveFormattedAsXml("returns", value, addIfMissing: false); - } - else - { - Log.Warning($"Attempted to save a returns item for a method that returns System.Void: {DocId}"); - } - } - } + public override string Returns => (ReturnType != "System.Void") ? GetNodesInPlainText("returns") : string.Empty; - public override string Remarks - { - get - { - return GetNodesInPlainText("remarks"); - } - set - { - SaveFormattedAsMarkdown("remarks", value, addIfMissing: !value.IsDocsEmpty(), isMember: false); - } - } + public override string Remarks => GetNodesInPlainText("remarks"); - public override string ToString() - { - return FullName; - } + public override List Exceptions { get; } = new(); + + public override string ToString() => FullName; protected override string GetApiSignatureDocId() { DocsTypeSignature? dts = TypeSignatures.FirstOrDefault(x => x.Language == "DocId"); - if (dts == null) - { - throw new FormatException($"DocId TypeSignature not found for {FullName}"); - } - return dts.Value; + return dts != null ? dts.Value : throw new FormatException($"DocId TypeSignature not found for {FullName}"); } } } diff --git a/src/PortToTripleSlash/src/libraries/Docs/DocsTypeParam.cs b/src/PortToTripleSlash/src/libraries/Docs/DocsTypeParam.cs index 54988bc..2e834fd 100644 --- a/src/PortToTripleSlash/src/libraries/Docs/DocsTypeParam.cs +++ b/src/PortToTripleSlash/src/libraries/Docs/DocsTypeParam.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Xml.Linq; @@ -11,31 +11,12 @@ namespace ApiDocsSync.PortToTripleSlash.Docs internal class DocsTypeParam { private readonly XElement XEDocsTypeParam; - public IDocsAPI ParentAPI - { - get; private set; - } - public string Name - { - get - { - return XmlHelper.GetAttributeValue(XEDocsTypeParam, "name"); - } - } + public IDocsAPI ParentAPI { get; } - public string Value - { - get - { - return XmlHelper.GetNodesInPlainText(XEDocsTypeParam); - } - set - { - XmlHelper.SaveFormattedAsXml(XEDocsTypeParam, value); - ParentAPI.Changed = true; - } - } + public string Name => XmlHelper.GetAttributeValue(XEDocsTypeParam, "name"); + + public string Value => XmlHelper.GetNodesInPlainText("typeparam", XEDocsTypeParam); public DocsTypeParam(IDocsAPI parentAPI, XElement xeDocsTypeParam) { diff --git a/src/PortToTripleSlash/src/libraries/Docs/DocsTypeParameter.cs b/src/PortToTripleSlash/src/libraries/Docs/DocsTypeParameter.cs index 1f70a88..b84d0db 100644 --- a/src/PortToTripleSlash/src/libraries/Docs/DocsTypeParameter.cs +++ b/src/PortToTripleSlash/src/libraries/Docs/DocsTypeParameter.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; @@ -13,55 +13,18 @@ namespace ApiDocsSync.PortToTripleSlash.Docs internal class DocsTypeParameter { private readonly XElement XETypeParameter; - public string Name - { - get - { - return XmlHelper.GetAttributeValue(XETypeParameter, "Name"); - } - } - private XElement? Constraints - { - get - { - return XETypeParameter.Element("Constraints"); - } - } + + public string Name => XmlHelper.GetAttributeValue(XETypeParameter, "Name"); + + private XElement? Constraints => XETypeParameter.Element("Constraints"); + private List? _constraintsParameterAttributes; - public List ConstraintsParameterAttributes - { - get - { - if (_constraintsParameterAttributes == null) - { - if (Constraints != null) - { - _constraintsParameterAttributes = Constraints.Elements("ParameterAttribute").Select(x => XmlHelper.GetNodesInPlainText(x)).ToList(); - } - else - { - _constraintsParameterAttributes = new List(); - } - } - return _constraintsParameterAttributes; - } - } + public List ConstraintsParameterAttributes => _constraintsParameterAttributes ??= Constraints != null + ? Constraints.Elements("ParameterAttribute").Select(x => XmlHelper.GetNodesInPlainText("ParameterAttribute", x)).ToList() + : new List(); - public string ConstraintsBaseTypeName - { - get - { - if (Constraints != null) - { - return XmlHelper.GetChildElementValue(Constraints, "BaseTypeName"); - } - return string.Empty; - } - } + public string ConstraintsBaseTypeName => Constraints != null ? XmlHelper.GetChildElementValue(Constraints, "BaseTypeName") : string.Empty; - public DocsTypeParameter(XElement xeTypeParameter) - { - XETypeParameter = xeTypeParameter; - } + public DocsTypeParameter(XElement xeTypeParameter) => XETypeParameter = xeTypeParameter; } } diff --git a/src/PortToTripleSlash/src/libraries/Docs/DocsTypeSignature.cs b/src/PortToTripleSlash/src/libraries/Docs/DocsTypeSignature.cs index 48db9b2..84109bf 100644 --- a/src/PortToTripleSlash/src/libraries/Docs/DocsTypeSignature.cs +++ b/src/PortToTripleSlash/src/libraries/Docs/DocsTypeSignature.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Xml.Linq; @@ -9,25 +9,10 @@ internal class DocsTypeSignature { private readonly XElement XETypeSignature; - public string Language - { - get - { - return XmlHelper.GetAttributeValue(XETypeSignature, "Language"); - } - } + public string Language => XmlHelper.GetAttributeValue(XETypeSignature, "Language"); - public string Value - { - get - { - return XmlHelper.GetAttributeValue(XETypeSignature, "Value"); - } - } + public string Value => XmlHelper.GetAttributeValue(XETypeSignature, "Value"); - public DocsTypeSignature(XElement xeTypeSignature) - { - XETypeSignature = xeTypeSignature; - } + public DocsTypeSignature(XElement xeTypeSignature) => XETypeSignature = xeTypeSignature; } } diff --git a/src/PortToTripleSlash/src/libraries/Docs/IDocsAPI.cs b/src/PortToTripleSlash/src/libraries/Docs/IDocsAPI.cs index 79d39f6..ff3f81f 100644 --- a/src/PortToTripleSlash/src/libraries/Docs/IDocsAPI.cs +++ b/src/PortToTripleSlash/src/libraries/Docs/IDocsAPI.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; @@ -10,7 +10,6 @@ internal interface IDocsAPI { public abstract APIKind Kind { get; } public abstract bool IsUndocumented { get; } - public abstract bool Changed { get; set; } public abstract string FilePath { get; set; } public abstract string DocId { get; } public abstract string DocIdUnprefixed { get; } @@ -19,11 +18,11 @@ internal interface IDocsAPI public abstract List Params { get; } public abstract List TypeParameters { get; } public abstract List TypeParams { get; } - public abstract string Summary { get; set; } + public abstract string Summary { get; } + public abstract string Value { get; } public abstract string ReturnType { get; } - public abstract string Returns { get; set; } - public abstract string Remarks { get; set; } - public abstract DocsParam SaveParam(XElement xeCoreFXParam); - public abstract DocsTypeParam AddTypeParam(string name, string value); + public abstract string Returns { get; } + public abstract string Remarks { get; } + public abstract List Exceptions { get; } } } diff --git a/src/PortToTripleSlash/src/libraries/ResolvedLocation.cs b/src/PortToTripleSlash/src/libraries/ResolvedLocation.cs index 1e4ec7f..92710c9 100644 --- a/src/PortToTripleSlash/src/libraries/ResolvedLocation.cs +++ b/src/PortToTripleSlash/src/libraries/ResolvedLocation.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.CodeAnalysis; @@ -7,19 +7,20 @@ namespace ApiDocsSync.PortToTripleSlash { public class ResolvedLocation { - public string TypeName { get; private set; } - public Compilation Compilation { get; private set; } - public Location Location { get; private set; } - public SyntaxTree Tree { get; set; } - public SemanticModel Model { get; set; } + public string TypeName { get; } + public Compilation Compilation { get; } + public Location Location { get; } + public SyntaxTree Tree { get; } + public SemanticModel Model { get; } public SyntaxNode? NewNode { get; set; } + public ResolvedLocation(string typeName, Compilation compilation, Location location, SyntaxTree tree) { TypeName = typeName; Compilation = compilation; Location = location; Tree = tree; - Model = compilation.GetSemanticModel(Tree); + Model = Compilation.GetSemanticModel(Tree); } } } diff --git a/src/PortToTripleSlash/src/libraries/ResolvedProject.cs b/src/PortToTripleSlash/src/libraries/ResolvedProject.cs index afeb68d..a023e1c 100644 --- a/src/PortToTripleSlash/src/libraries/ResolvedProject.cs +++ b/src/PortToTripleSlash/src/libraries/ResolvedProject.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.CodeAnalysis; @@ -7,10 +7,11 @@ namespace ApiDocsSync.PortToTripleSlash { public class ResolvedProject { - public ResolvedWorkspace ResolvedWorkspace { get; private set; } - public Project Project { get; private set; } - public Compilation Compilation { get; private set; } - public string ProjectPath { get; private set; } + public ResolvedWorkspace ResolvedWorkspace { get; } + public Project Project { get; } + public Compilation Compilation { get; } + public string ProjectPath { get; } + public ResolvedProject(ResolvedWorkspace resolvedWorkspace, string projectPath, Project project, Compilation compilation) { ResolvedWorkspace = resolvedWorkspace; diff --git a/src/PortToTripleSlash/src/libraries/ResolvedWorkspace.cs b/src/PortToTripleSlash/src/libraries/ResolvedWorkspace.cs index 8528659..2d9f7d3 100644 --- a/src/PortToTripleSlash/src/libraries/ResolvedWorkspace.cs +++ b/src/PortToTripleSlash/src/libraries/ResolvedWorkspace.cs @@ -1,19 +1,24 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.MSBuild; namespace ApiDocsSync.PortToTripleSlash { public class ResolvedWorkspace { - public MSBuildWorkspace Workspace { get; private set; } + public MSBuildWorkspace Workspace { get; } public List ResolvedProjects { get; } + public SyntaxGenerator Generator { get; } + public ResolvedWorkspace(MSBuildWorkspace workspace) { Workspace = workspace; ResolvedProjects = new List(); + Generator = SyntaxGenerator.GetGenerator(workspace, LanguageNames.CSharp); } } } diff --git a/src/PortToTripleSlash/src/libraries/RoslynTripleSlash/DocumentationUpdater.cs b/src/PortToTripleSlash/src/libraries/RoslynTripleSlash/DocumentationUpdater.cs new file mode 100644 index 0000000..651529f --- /dev/null +++ b/src/PortToTripleSlash/src/libraries/RoslynTripleSlash/DocumentationUpdater.cs @@ -0,0 +1,442 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Xml.Linq; +using ApiDocsSync.PortToTripleSlash.Docs; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static System.Net.Mime.MediaTypeNames; + +namespace ApiDocsSync.PortToTripleSlash.Roslyn; + +internal class DocumentationUpdater +{ + private const string TripleSlash = "///"; + private const string Space = " "; + private const string NewLine = "\n"; + private static readonly char[] _NewLineSeparators = ['\n', '\r']; + private const StringSplitOptions _NewLineSplitOptions = StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries; + + private readonly Configuration _config; + private readonly IDocsAPI _api; + private readonly SyntaxTrivia _indentationTrivia; + + public DocumentationUpdater(Configuration config, IDocsAPI api, SyntaxTrivia? indentationTrivia) + { + _config = config; + _api = api; + _indentationTrivia = indentationTrivia.HasValue ? indentationTrivia.Value : SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, string.Empty); + } + + public DocumentationCommentTriviaSyntax GetUpdatedDocs(SyntaxList originalDocumentation) + { + List docsNodes = []; + + // Preserve the order in which each API element is looked for below + + if (!_api.Summary.IsDocsEmpty()) + { + docsNodes.Add(GetSummaryNodeFromDocs()); + } + else if (TryGet("summary") is XmlNodeSyntax existingSummary) + { + docsNodes.Add(GetExistingElementWithRequiredTrivia(existingSummary)); + } + + if (!_api.Value.IsDocsEmpty()) + { + docsNodes.Add(GetValueNodeFromDocs()); + } + else if (TryGet("value") is XmlNodeSyntax existingValue) + { + docsNodes.Add(GetExistingElementWithRequiredTrivia(existingValue)); + } + + foreach (DocsTypeParam typeParam in _api.TypeParams) + { + if (!typeParam.Value.IsDocsEmpty()) + { + docsNodes.Add(GetTypeParamNode(typeParam)); + } + else if (TryGet("typeparam", "name", typeParam.Value) is XmlNodeSyntax existingTypeParam) + { + docsNodes.Add(GetExistingElementWithRequiredTrivia(existingTypeParam)); + + } + } + + foreach (DocsParam param in _api.Params) + { + if (!param.Value.IsDocsEmpty()) + { + docsNodes.Add(GetParamNode(param)); + } + else if (TryGet("param", "name", param.Value) is XmlNodeSyntax existingParam) + { + docsNodes.Add(GetExistingElementWithRequiredTrivia(existingParam)); + + } + } + + if (!_api.Returns.IsDocsEmpty()) + { + docsNodes.Add(GetReturnsNodeFromDocs()); + } + else if (TryGet("returns") is XmlNodeSyntax existingReturns) + { + docsNodes.Add(GetExistingElementWithRequiredTrivia(existingReturns)); + } + + foreach (DocsException exception in _api.Exceptions) + { + if (!exception.Value.IsDocsEmpty()) + { + docsNodes.Add(GetExceptionNode(exception)); + } + else if (TryGet("exception", "cref", exception.Value) is XmlNodeSyntax existingException) + { + docsNodes.Add(GetExistingElementWithRequiredTrivia(existingException)); + } + } + + // Only port them if that's the desired action, otherwise, preserve the existing ones + if (!_config.SkipRemarks) + { + if (!_api.Remarks.IsDocsEmpty()) + { + docsNodes.Add(GetRemarksNodeFromDocs()); + } + else if (TryGet("remarks") is XmlNodeSyntax existingRemarks) + { + docsNodes.Add(GetExistingElementWithRequiredTrivia(existingRemarks)); + } + } + else if (TryGet("remarks") is XmlNodeSyntax existingRemarks) + { + docsNodes.Add(GetExistingElementWithRequiredTrivia(existingRemarks)); + } + + return SyntaxFactory.DocumentationCommentTrivia( + SyntaxKind.SingleLineDocumentationCommentTrivia, + SyntaxFactory.List(docsNodes)); + + XmlNodeSyntax? TryGet(string tagName, string? attributeName = null, string? attributeValue = null) + { + return originalDocumentation.FirstOrDefault(xmlNode => DoesNodeHaveTag(xmlNode, tagName, attributeName, attributeValue)); + } + } + + public DocumentationCommentTriviaSyntax GetNewDocs() + { + List nodes = new(); + + // Preserve the order + if (!_api.Summary.IsDocsEmpty()) + { + nodes.Add(GetSummaryNodeFromDocs()); + } + if (!_api.Value.IsDocsEmpty()) + { + nodes.Add(GetValueNodeFromDocs()); + } + if (_api.TypeParams.Any()) + { + nodes.AddRange(GetTypeParamNodesFromDocs()); + } + if (_api.Params.Any()) + { + nodes.AddRange(GetParamNodesFromDocs()); + } + if (!_api.Returns.IsDocsEmpty()) + { + nodes.Add(GetReturnsNodeFromDocs()); + } + if (_api.Exceptions.Any()) + { + nodes.AddRange(GetExceptionNodesFromDocs()); + } + if (!_config.SkipRemarks && !_api.Remarks.IsDocsEmpty()) + { + nodes.Add(GetRemarksNodeFromDocs()); + } + + return SyntaxFactory.DocumentationCommentTrivia( + SyntaxKind.SingleLineDocumentationCommentTrivia, + SyntaxFactory.List(nodes)); + } + + private XmlNodeSyntax GetSummaryNodeFromDocs() + { + List internalTextNodes = []; + + bool startingTrivia = true; + foreach (string line in _api.Summary.Split(_NewLineSeparators, _NewLineSplitOptions)) + { + internalTextNodes.Add(GetFullTripleSlashSingleLineXmlTextSyntaxNode(line, startingTrivia)); + startingTrivia = false; + } + + return GetXmlAttributedElementNode(internalTextNodes, "summary", keepTagsInSameLine: false); + } + + private XmlNodeSyntax GetValueNodeFromDocs() + { + List internalTextNodes = GetNonSummaryFullTripleSlashSingleLineXmlTextSyntaxNodes(_api.Value); + return GetXmlAttributedElementNode(internalTextNodes, "value"); + } + + private XmlNodeSyntax[] GetTypeParamNodesFromDocs() + { + List typeParamNodes = new(); + foreach (DocsTypeParam typeParam in _api.TypeParams) + { + typeParamNodes.Add(GetTypeParamNode(typeParam)); + } + + return typeParamNodes.ToArray(); + } + + private XmlNodeSyntax GetTypeParamNode(DocsTypeParam typeParam) + { + List internalTextNodes = GetNonSummaryFullTripleSlashSingleLineXmlTextSyntaxNodes(typeParam.Value); + return GetXmlAttributedElementNode(internalTextNodes, "typeparam", "name", typeParam.Name); + } + + private XmlNodeSyntax[] GetParamNodesFromDocs() + { + List paramNodes = new(); + foreach (DocsParam param in _api.Params) + { + paramNodes.Add(GetParamNode(param)); + } + + return paramNodes.ToArray(); + } + + private XmlNodeSyntax GetParamNode(DocsParam param) + { + List internalTextNodes = GetNonSummaryFullTripleSlashSingleLineXmlTextSyntaxNodes(param.Value); + return GetXmlAttributedElementNode(internalTextNodes, "param", "name", param.Name); + } + + private XmlNodeSyntax GetReturnsNodeFromDocs() + { + List internalTextNodes = GetNonSummaryFullTripleSlashSingleLineXmlTextSyntaxNodes(_api.Returns); + return GetXmlAttributedElementNode(internalTextNodes, "returns"); + } + + private XmlNodeSyntax GetRemarksNodeFromDocs() + { + List internalTextNodes = GetNonSummaryFullTripleSlashSingleLineXmlTextSyntaxNodes(_api.Remarks); + return GetXmlAttributedElementNode(internalTextNodes, "remarks"); + } + + private XmlNodeSyntax[] GetExceptionNodesFromDocs() + { + List exceptionNodes = new(); + foreach (DocsException exception in _api.Exceptions) + { + exceptionNodes.Add(GetExceptionNode(exception)); + } + + return exceptionNodes.ToArray(); + } + + private XmlNodeSyntax GetExceptionNode(DocsException exception) + { + List internalTextNodes = GetNonSummaryFullTripleSlashSingleLineXmlTextSyntaxNodes(exception.Value); + return GetXmlAttributedElementNode(internalTextNodes, "exception", "cref", exception.Cref[2..]); + } + + private XmlNodeSyntax GetXmlAttributedElementNode(IEnumerable content, string tagName, string? attributeName = null, string? attributeValue = null, bool keepTagsInSameLine = true) + { + Debug.Assert(!string.IsNullOrWhiteSpace(tagName)); + + GetLeadingTrivia(out SyntaxTriviaList leadingTrivia); + GetTrailingTrivia(out SyntaxTriviaList trailingTrivia); + + XmlElementStartTagSyntax startTag = SyntaxFactory + .XmlElementStartTag(SyntaxFactory.XmlName(SyntaxFactory.Identifier(tagName))) + .WithLeadingTrivia(leadingTrivia); + + if (!keepTagsInSameLine) + { + startTag = startTag.WithTrailingTrivia(trailingTrivia); + } + + if (!string.IsNullOrWhiteSpace(attributeName)) + { + Debug.Assert(!string.IsNullOrWhiteSpace(attributeValue)); + + SyntaxToken xmlAttributeName = SyntaxFactory.Identifier( + leading: SyntaxFactory.TriviaList(SyntaxFactory.Space), + text: attributeName, + trailing: SyntaxFactory.TriviaList()); + + XmlNameAttributeSyntax xmlAttribute = SyntaxFactory.XmlNameAttribute( + name: SyntaxFactory.XmlName(xmlAttributeName), + startQuoteToken: SyntaxFactory.Token(SyntaxKind.DoubleQuoteToken), + identifier: SyntaxFactory.IdentifierName(attributeValue), + endQuoteToken: SyntaxFactory.Token(SyntaxKind.DoubleQuoteToken)); + + startTag = startTag.WithAttributes(SyntaxFactory.List([xmlAttribute])); + } + + XmlElementEndTagSyntax endTag = SyntaxFactory + .XmlElementEndTag(SyntaxFactory.XmlName(SyntaxFactory.Identifier(tagName))) + .WithTrailingTrivia(trailingTrivia); + + if (!keepTagsInSameLine) + { + endTag = endTag.WithLeadingTrivia(leadingTrivia); + } + + return SyntaxFactory.XmlElement(startTag, SyntaxFactory.List(content), endTag); + } + + private XmlNodeSyntax GetExistingElementWithRequiredTrivia(XmlNodeSyntax existingNode) + { + GetLeadingTrivia(out SyntaxTriviaList leadingTrivia); + GetTrailingTrivia(out SyntaxTriviaList trailingTrivia); + return existingNode.WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTrivia); + } + + // Returns a single line of optional indentaiton, optional triple slashes, the optional line of text that may follow it, and the optional newline. + // Examples: + // - For the summary tag, leadingTrivia must always be true and trailingTrivia must always be true: + // [indentation][tripleslash][textline][newline] + // Example: ->->->/// text\n + // - For all other tags, leadingTrivia must only be false in the first item and trailingTrivia must be false in the last item: + // First item: [textline][newline] + // Example: text\n + // Last item: [indentation][tripleslash][textline] + // Example: ->->->/// text + private XmlTextSyntax GetFullTripleSlashSingleLineXmlTextSyntaxNode(string text, bool leadingTrivia = false, bool trailingTrivia = true) + { + GetIndentationSyntaxToken(out SyntaxToken indentationSyntaxToken); + GetTripleSlashSyntaxToken(out SyntaxToken tripleSlashSyntaxToken); + GetNewLineSyntaxToken(out SyntaxToken newLineSyntaxToken); + + List list = []; + + if (leadingTrivia) + { + list.Add(indentationSyntaxToken); + list.Add(tripleSlashSyntaxToken); + } + + list.Add(SyntaxFactory.XmlTextNewLine( + leading: SyntaxFactory.TriviaList(), + text: text, + value: text, + trailing: SyntaxFactory.TriviaList())); + + if (trailingTrivia) + { + list.Add(newLineSyntaxToken); + } + + return SyntaxFactory.XmlText(SyntaxFactory.TokenList(list)); + } + + private List GetNonSummaryFullTripleSlashSingleLineXmlTextSyntaxNodes(string text) + { + List nodes = []; + string[] splitted = text.Split(_NewLineSeparators, _NewLineSplitOptions); + for(int i = 0; i < splitted.Length; i++) + { + string line = splitted[i]; + nodes.Add(GetFullTripleSlashSingleLineXmlTextSyntaxNode(line, leadingTrivia: i > 0, trailingTrivia: i < (splitted.Length - 1))); + } + return nodes; + } + + // Returns a syntax node containing the "/// " text literal syntax token. + private XmlTextSyntax GetTripleSlashTextSyntaxNode() + { + GetTripleSlashSyntaxToken(out SyntaxToken tripleSlashSyntaxToken); + return SyntaxFactory.XmlText().WithTextTokens(SyntaxFactory.TokenList(tripleSlashSyntaxToken)); + } + + // Returns a syntax node containing the "\n" text literal syntax token. + private XmlTextSyntax GetNewLineTextSyntaxNode() + { + GetNewLineSyntaxToken(out SyntaxToken newLineSyntaxToken); + return SyntaxFactory.XmlText().WithTextTokens(SyntaxFactory.TokenList(newLineSyntaxToken)); + } + + // Returns a syntax node containing the specified indentation text literal syntax token. + private XmlTextSyntax GetIndentationTextSyntaxNode() + { + GetIndentationSyntaxToken(out SyntaxToken indentationSyntaxToken); + return SyntaxFactory.XmlText().WithTextTokens(SyntaxFactory.TokenList(indentationSyntaxToken)); + } + + // Returns a syntax token containing the "/// " text literal. + private void GetTripleSlashSyntaxToken(out SyntaxToken tripleSlashSyntaxToken) => + tripleSlashSyntaxToken = SyntaxFactory.XmlTextLiteral( + leading: SyntaxFactory.TriviaList(SyntaxFactory.DocumentationCommentExterior(TripleSlash)), + text: Space, + value: Space, + trailing: SyntaxFactory.TriviaList()); + + // Returns a syntax token containing the "\n" text literal. + private void GetNewLineSyntaxToken(out SyntaxToken newLineSyntaxToken) => + newLineSyntaxToken = SyntaxFactory.XmlTextNewLine( + leading: SyntaxFactory.TriviaList(), + text: NewLine, + value: NewLine, + trailing: SyntaxFactory.TriviaList()); + + // Returns a syntax token with the "" text literal preceded by the specified indentation trivia. + private void GetIndentationSyntaxToken(out SyntaxToken indentationSyntaxToken) => + indentationSyntaxToken = SyntaxFactory.XmlTextLiteral( + leading: SyntaxFactory.TriviaList(_indentationTrivia), + text: string.Empty, + value: string.Empty, + trailing: SyntaxFactory.TriviaList()); + + private void GetLeadingTrivia(out SyntaxTriviaList leadingTrivia) + { + leadingTrivia = SyntaxFactory.TriviaList( + SyntaxFactory.Trivia( + SyntaxFactory.DocumentationCommentTrivia( + SyntaxKind.SingleLineDocumentationCommentTrivia, + SyntaxFactory.List([GetIndentationTextSyntaxNode(), GetTripleSlashTextSyntaxNode()])))); + } + + private void GetTrailingTrivia(out SyntaxTriviaList trailingTrivia) + { + trailingTrivia = SyntaxFactory.TriviaList( + SyntaxFactory.Trivia( + SyntaxFactory.DocumentationCommentTrivia( + SyntaxKind.SingleLineDocumentationCommentTrivia, + SyntaxFactory.SingletonList(GetNewLineTextSyntaxNode())))); + } + + private static bool DoesNodeHaveTag(SyntaxNode xmlNode, string tagName, string? attributeName = null, string? attributeValue = null) + { + if (xmlNode.Kind() is SyntaxKind.XmlElement && xmlNode is XmlElementSyntax xmlElement) + { + bool hasNodeWithTag = xmlElement.StartTag.Name.LocalName.ValueText == tagName; + + // No attribute passed, we just want to check tag name + if (string.IsNullOrWhiteSpace(attributeName)) + { + return hasNodeWithTag; + } + + // To check attribute, attributeValue must also be passed + return !string.IsNullOrWhiteSpace(attributeValue) && + xmlElement.StartTag.Attributes.FirstOrDefault(a => a.Name.LocalName.ValueText == attributeName) is XmlTextAttributeSyntax xmlAttribute && + xmlAttribute.TextTokens.ToString() == attributeValue; + } + + return false; + } +} diff --git a/src/PortToTripleSlash/src/libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/src/PortToTripleSlash/src/libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index 705f3ec..8e2c18e 100644 --- a/src/PortToTripleSlash/src/libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/src/PortToTripleSlash/src/libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -1,1047 +1,396 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text.RegularExpressions; using ApiDocsSync.PortToTripleSlash.Docs; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using static System.Net.Mime.MediaTypeNames; -namespace ApiDocsSync.PortToTripleSlash.Roslyn -{ - /* - The following triple slash comments section: - - /// - /// My summary. - /// - /// My param description. - /// My remarks. - public ... - - translates to this syntax tree structure: +namespace ApiDocsSync.PortToTripleSlash.Roslyn; - PublicKeyword (SyntaxToken) -> The public keyword including its trivia. - Lead: EndOfLineTrivia -> The newline char before the 4 whitespace chars before the triple slash comments. - Lead: WhitespaceTrivia -> The 4 whitespace chars before the triple slash comments. - Lead: SingleLineDocumentationCommentTrivia (SyntaxTrivia) - SingleLineDocumentationCommentTrivia (DocumentationCommentTriviaSyntax) -> The triple slash comments, excluding the first 3 slash chars. - XmlText (XmlTextSyntax) - XmlTextLiteralToken (SyntaxToken) -> The space between the first triple slash and . - Lead: DocumentationCommentExteriorTrivia (SyntaxTrivia) -> The first 3 slash chars. +/* + * According to the Roslyn Quoter: https://roslynquoter.azurewebsites.net/ + * This code: - XmlElement (XmlElementSyntax) -> From to . Excludes the first 3 slash chars, but includes the second and third trios. - XmlElementStartTag (XmlElementStartTagSyntax) -> - LessThanToken (SyntaxToken) -> < - XmlName (XmlNameSyntax) -> summary - IdentifierToken (SyntaxToken) -> summary - GreaterThanToken (SyntaxToken) -> > - XmlText (XmlTextSyntax) -> Everything after and before - XmlTextLiteralNewLineToken (SyntaxToken) -> endline after - XmlTextLiteralToken (SyntaxToken) -> [ My summary.] - Lead: DocumentationCommentExteriorTrivia (SyntaxTrivia) -> endline after summary text - XmlTextLiteralNewToken (SyntaxToken) -> Space between 3 slashes and - Lead: DocumentationCommentExteriorTrivia (SyntaxTrivia) -> whitespace + 3 slashes before the - XmlElementEndTag (XmlElementEndTagSyntax) -> - LessThanSlashToken (SyntaxToken) -> summary - IdentifierToken (SyntaxToken) -> summary - GreaterThanToken (SyntaxToken) -> > - XmlText -> endline + whitespace + 3 slahes before endline after - XmlTextLiteralToken (XmlTextLiteralToken) -> space after 3 slashes and before whitespace + 3 slashes before the space and ... - XmlElementStartTag -> - LessThanToken -> < - XmlName -> param - IdentifierToken -> param - XmlNameAttribute (XmlNameAttributeSyntax) -> name="paramName" - XmlName -> name - IdentifierToken -> name - Lead: WhitespaceTrivia -> space between param and name - EqualsToken -> = - DoubleQuoteToken -> opening " - IdentifierName -> paramName - IdentifierToken -> paramName - DoubleQuoteToken -> closing " - GreaterThanToken -> > - XmlText -> My param description. - XmlTextLiteralToken -> My param description. - XmlElementEndTag -> - LessThanSlashToken -> param - IdentifierToken -> param - GreaterThanToken -> > - XmlText -> newline + 4 whitespace chars + /// before +public class MyClass +{ + /// MySummary + /// MyParameter + public void MyMethod(int x) { } +} - XmlElement -> My remarks. - XmlText -> new line char after - XmlTextLiteralNewLineToken -> new line char after - EndOfDocumentationCommentToken (SyntaxToken) -> invisible + * Can be generated using: + +SyntaxFactory.CompilationUnit() +.WithMembers( + SyntaxFactory.SingletonList( + SyntaxFactory.ClassDeclaration("MyClass") + .WithModifiers( + SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword))) + .WithMembers( + SyntaxFactory.SingletonList( + SyntaxFactory.MethodDeclaration( + SyntaxFactory.PredefinedType( + SyntaxFactory.Token(SyntaxKind.VoidKeyword)), + SyntaxFactory.Identifier("MyMethod")) + .WithModifiers( + SyntaxFactory.TokenList( + SyntaxFactory.Token( + SyntaxFactory.TriviaList( + SyntaxFactory.Trivia( + SyntaxFactory.DocumentationCommentTrivia( + SyntaxKind.SingleLineDocumentationCommentTrivia, + SyntaxFactory.List( + new XmlNodeSyntax[]{ + SyntaxFactory.XmlText() + .WithTextTokens( + SyntaxFactory.TokenList( + SyntaxFactory.XmlTextLiteral( + SyntaxFactory.TriviaList( + SyntaxFactory.DocumentationCommentExterior("///")), + " ", + " ", + SyntaxFactory.TriviaList()))), + SyntaxFactory.XmlExampleElement( + SyntaxFactory.SingletonList( + SyntaxFactory.XmlText() + .WithTextTokens( + SyntaxFactory.TokenList( + SyntaxFactory.XmlTextLiteral( + SyntaxFactory.TriviaList(), + "MySummary", + "MySummary", + SyntaxFactory.TriviaList()))))) + .WithStartTag( + SyntaxFactory.XmlElementStartTag( + SyntaxFactory.XmlName( + SyntaxFactory.Identifier("summary")))) + .WithEndTag( + SyntaxFactory.XmlElementEndTag( + SyntaxFactory.XmlName( + SyntaxFactory.Identifier("summary")))), + SyntaxFactory.XmlText() + .WithTextTokens( + SyntaxFactory.TokenList( + new []{ + SyntaxFactory.XmlTextNewLine( + SyntaxFactory.TriviaList(), + "\n", + "\n", + SyntaxFactory.TriviaList()), + SyntaxFactory.XmlTextLiteral( + SyntaxFactory.TriviaList( + SyntaxFactory.DocumentationCommentExterior(" ///")), + " ", + " ", + SyntaxFactory.TriviaList())})), + SyntaxFactory.XmlExampleElement( + SyntaxFactory.SingletonList( + SyntaxFactory.XmlText() + .WithTextTokens( + SyntaxFactory.TokenList( + SyntaxFactory.XmlTextLiteral( + SyntaxFactory.TriviaList(), + "MyParameter", + "MyParameter", + SyntaxFactory.TriviaList()))))) + .WithStartTag( + SyntaxFactory.XmlElementStartTag( + SyntaxFactory.XmlName( + SyntaxFactory.Identifier( + SyntaxFactory.TriviaList(), + SyntaxKind.ParamKeyword, + "param", + "param", + SyntaxFactory.TriviaList()))) + .WithAttributes( + SyntaxFactory.SingletonList( + SyntaxFactory.XmlNameAttribute( + SyntaxFactory.XmlName( + SyntaxFactory.Identifier("name")), + SyntaxFactory.Token(SyntaxKind.DoubleQuoteToken), + SyntaxFactory.IdentifierName("x"), + SyntaxFactory.Token(SyntaxKind.DoubleQuoteToken))))) + .WithEndTag( + SyntaxFactory.XmlElementEndTag( + SyntaxFactory.XmlName( + SyntaxFactory.Identifier( + SyntaxFactory.TriviaList(), + SyntaxKind.ParamKeyword, + "param", + "param", + SyntaxFactory.TriviaList())))), + SyntaxFactory.XmlText() + .WithTextTokens( + SyntaxFactory.TokenList( + SyntaxFactory.XmlTextNewLine( + SyntaxFactory.TriviaList(), + "\n", + "\n", + SyntaxFactory.TriviaList())))})))), + SyntaxKind.PublicKeyword, + SyntaxFactory.TriviaList()))) + .WithParameterList( + SyntaxFactory.ParameterList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Parameter( + SyntaxFactory.Identifier("x")) + .WithType( + SyntaxFactory.PredefinedType( + SyntaxFactory.Token(SyntaxKind.IntKeyword)))))) + .WithBody( + SyntaxFactory.Block()))))) +.NormalizeWhitespace() +*/ + +internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter +{ + private DocsCommentsContainer DocsComments { get; } + private ResolvedLocation Location { get; } + private SemanticModel Model => Location.Model; - Lead: WhitespaceTrivia -> The 4 whitespace chars before the public keyword. - Trail: WhitespaceTrivia -> The single whitespace char after the public keyword. - */ - internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter + public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, ResolvedLocation resolvedLocation) : base(visitIntoStructuredTrivia: false) { - #region Private members + DocsComments = docsComments; + Location = resolvedLocation; + } - private static readonly string UnixNewLine = "\n"; + public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) => VisitType(node, base.VisitClassDeclaration(node)); - private static readonly string[] ReservedKeywords = new[] { "abstract", "async", "await", "false", "null", "sealed", "static", "true", "virtual" }; + public override SyntaxNode? VisitDelegateDeclaration(DelegateDeclarationSyntax node) => VisitType(node, base.VisitDelegateDeclaration(node)); - private static readonly string[] MarkdownUnconvertableStrings = new[] { "](~/includes", "[!INCLUDE" }; + public override SyntaxNode? VisitEnumDeclaration(EnumDeclarationSyntax node) => VisitType(node, base.VisitEnumDeclaration(node)); - private static readonly string[] MarkdownCodeIncludes = new[] { "[!code-cpp", "[!code-csharp", "[!code-vb", }; + public override SyntaxNode? VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) => VisitType(node, base.VisitInterfaceDeclaration(node)); - private static readonly string[] MarkdownExamples = new[] { "## Examples", "## Example" }; + public override SyntaxNode? VisitRecordDeclaration(RecordDeclarationSyntax node) => VisitType(node, base.VisitRecordDeclaration(node)); - private static readonly string[] MarkdownHeaders = new[] { "[!NOTE]", "[!IMPORTANT]", "[!TIP]" }; + public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node) => VisitType(node, base.VisitStructDeclaration(node)); - // Note that we need to support generics that use the ` literal as well as the escaped %60 - private static readonly string ValidRegexChars = @"[A-Za-z0-9\-\._~:\/#\[\]\{\}@!\$&'\(\)\*\+,;]|(%60|`)\d+"; - private static readonly string ValidExtraChars = @"\?="; + public override SyntaxNode? VisitEventFieldDeclaration(EventFieldDeclarationSyntax node) => VisitVariableDeclaration(node, base.VisitEventFieldDeclaration(node)); - private static readonly string RegexDocIdPattern = @"(?[A-Za-z]{1}:)?(?(" + ValidRegexChars + @")+)(?%2[aA])?(?\?(" + ValidRegexChars + @")+=(" + ValidRegexChars + @")+)?"; - private static readonly string RegexXmlCrefPattern = "cref=\"" + RegexDocIdPattern + "\""; - private static readonly string RegexMarkdownXrefPattern = @"(?)"; + public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node) => VisitVariableDeclaration(node, base.VisitFieldDeclaration(node)); - private static readonly string RegexMarkdownBoldPattern = @"\*\*(?[A-Za-z0-9\-\._~:\/#\[\]@!\$&'\(\)\+,;%` ]+)\*\*"; - private static readonly string RegexXmlBoldReplacement = @"${content}"; + public override SyntaxNode? VisitConstructorDeclaration(ConstructorDeclarationSyntax node) => VisitBaseMethodDeclaration(node, base.VisitConstructorDeclaration(node)); - private static readonly string RegexMarkdownLinkPattern = @"\[(?.+)\]\((?(http|www)(" + ValidRegexChars + "|" + ValidExtraChars + @")+)\)"; - private static readonly string RegexHtmlLinkReplacement = "${linkValue}"; + public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) => VisitBaseMethodDeclaration(node, base.VisitMethodDeclaration(node)); - private static readonly string RegexMarkdownCodeStartPattern = @"```(?(cs|csharp|cpp|vb|visualbasic))(?\s+)"; - private static readonly string RegexXmlCodeStartReplacement = "${spaces}"; + // TODO: Add test + public override SyntaxNode? VisitConversionOperatorDeclaration(ConversionOperatorDeclarationSyntax node) => VisitBaseMethodDeclaration(node, base.VisitConversionOperatorDeclaration(node)); - private static readonly string RegexMarkdownCodeEndPattern = @"```(?\s+)"; - private static readonly string RegexXmlCodeEndReplacement = "${spaces}"; + // TODO: Add test + public override SyntaxNode? VisitIndexerDeclaration(IndexerDeclarationSyntax node) => VisitBaseMethodDeclaration(node, base.VisitIndexerDeclaration(node)); - private static readonly Dictionary PrimitiveTypes = new() - { - { "System.Boolean", "bool" }, - { "System.Byte", "byte" }, - { "System.Char", "char" }, - { "System.Decimal", "decimal" }, - { "System.Double", "double" }, - { "System.Int16", "short" }, - { "System.Int32", "int" }, - { "System.Int64", "long" }, - { "System.Object", "object" }, // Ambiguous: could be 'object' or 'dynamic' https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/built-in-types - { "System.SByte", "sbyte" }, - { "System.Single", "float" }, - { "System.String", "string" }, - { "System.UInt16", "ushort" }, - { "System.UInt32", "uint" }, - { "System.UInt64", "ulong" }, - { "System.Void", "void" } - }; + public override SyntaxNode? VisitOperatorDeclaration(OperatorDeclarationSyntax node) => VisitBaseMethodDeclaration(node, base.VisitOperatorDeclaration(node)); - private DocsCommentsContainer DocsComments { get; } - private SemanticModel Model { get; } + public override SyntaxNode? VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) => VisitMemberDeclaration(node, base.VisitEnumMemberDeclaration(node)); - #endregion + public override SyntaxNode? VisitPropertyDeclaration(PropertyDeclarationSyntax node) => VisitBasePropertyDeclaration(node, base.VisitPropertyDeclaration(node)); - public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticModel model) : base(visitIntoStructuredTrivia: true) + private SyntaxNode? VisitType(SyntaxNode originalNode, SyntaxNode? baseNode) + { + if (!TryGetType(originalNode, out DocsType? type) || baseNode == null) { - DocsComments = docsComments; - Model = model; + return originalNode; } + return Generate(baseNode, type); + } - #region Visitor overrides - - public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) + private SyntaxNode? VisitBaseMethodDeclaration(SyntaxNode originalNode, SyntaxNode? baseNode) + { + // The Docs files only contain docs for public elements, + // so if no comments are found, we return the node unmodified + if (!TryGetMember(originalNode, out DocsMember? member) || baseNode == null) { - SyntaxNode? baseNode = base.VisitClassDeclaration(node); - - ISymbol? symbol = Model.GetDeclaredSymbol(node); - if (symbol == null) - { - Log.Warning($"Symbol is null."); - return baseNode; - } - - return VisitType(baseNode, symbol); + return originalNode; } + return Generate(baseNode, member); + } - public override SyntaxNode? VisitConstructorDeclaration(ConstructorDeclarationSyntax node) => - VisitBaseMethodDeclaration(node); - - public override SyntaxNode? VisitDelegateDeclaration(DelegateDeclarationSyntax node) + private SyntaxNode? VisitBasePropertyDeclaration(SyntaxNode originalNode, SyntaxNode? baseNode) + { + if (!TryGetMember(originalNode, out DocsMember? member) || baseNode == null) { - SyntaxNode? baseNode = base.VisitDelegateDeclaration(node); - - ISymbol? symbol = Model.GetDeclaredSymbol(node); - if (symbol == null) - { - Log.Warning($"Symbol is null."); - return baseNode; - } - - return VisitType(baseNode, symbol); + return originalNode; } + return Generate(baseNode, member); + } - public override SyntaxNode? VisitEnumDeclaration(EnumDeclarationSyntax node) + private SyntaxNode? VisitMemberDeclaration(SyntaxNode originalNode, SyntaxNode? baseNode) + { + if (!TryGetMember(originalNode, out DocsMember? member) || baseNode == null) { - SyntaxNode? baseNode = base.VisitEnumDeclaration(node); - - ISymbol? symbol = Model.GetDeclaredSymbol(node); - if (symbol == null) - { - Log.Warning($"Symbol is null."); - return baseNode; - } - - return VisitType(baseNode, symbol); + return originalNode; } + return Generate(baseNode, member); + } - public override SyntaxNode? VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) => - VisitMemberDeclaration(node); - - public override SyntaxNode? VisitEventFieldDeclaration(EventFieldDeclarationSyntax node) => - VisitVariableDeclaration(node); - - public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node) => - VisitVariableDeclaration(node); - - public override SyntaxNode? VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) + private SyntaxNode? VisitVariableDeclaration(SyntaxNode originalNode, SyntaxNode? baseNode) + { + if (!TryGetMember(originalNode, out DocsMember? member) || baseNode == null) { - SyntaxNode? baseNode = base.VisitInterfaceDeclaration(node); - - ISymbol? symbol = Model.GetDeclaredSymbol(node); - if (symbol == null) - { - Log.Warning($"Symbol is null."); - return baseNode; - } - - return VisitType(baseNode, symbol); + return originalNode; } - public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) => - VisitBaseMethodDeclaration(node); + return Generate(baseNode, member); + } - public override SyntaxNode? VisitOperatorDeclaration(OperatorDeclarationSyntax node) => - VisitBaseMethodDeclaration(node); + private bool TryGetMember(SyntaxNode originalNode, [NotNullWhen(returnValue: true)] out DocsMember? member) + { + member = null; - public override SyntaxNode? VisitPropertyDeclaration(PropertyDeclarationSyntax node) + SyntaxNode nodeWithSymbol; + if (originalNode is BaseFieldDeclarationSyntax fieldDecl) { - if (!TryGetMember(node, out DocsMember? member)) + // Special case: fields could be grouped in a single line if they all share the same data type + if (!IsPublic(fieldDecl)) { - return node; + return false; } - SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList leadingTrivia = node.GetLeadingTrivia(); - - SyntaxTriviaList summary = GetSummary(leadingTrivia, member, leadingWhitespace); - SyntaxTriviaList value = GetValue(leadingTrivia, member, leadingWhitespace); - SyntaxTriviaList exceptions = GetExceptions(leadingTrivia, member.Exceptions, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(leadingTrivia, member, leadingWhitespace); - SyntaxTriviaList seealsos = GetSeeAlsos(leadingTrivia, member.SeeAlsoCrefs, leadingWhitespace); - SyntaxTriviaList altmembers = GetAltMembers(leadingTrivia, member.AltMembers, leadingWhitespace); - SyntaxTriviaList relateds = GetRelateds(leadingTrivia, member.Relateds, leadingWhitespace); - - return GetNodeWithTrivia(leadingWhitespace, node, summary, value, exceptions, remarks, seealsos, altmembers, relateds); - } - - public override SyntaxNode? VisitRecordDeclaration(RecordDeclarationSyntax node) - { - SyntaxNode? baseNode = base.VisitRecordDeclaration(node); - - ISymbol? symbol = Model.GetDeclaredSymbol(node); - if (symbol == null) + VariableDeclarationSyntax variableDecl = fieldDecl.Declaration; + if (variableDecl.Variables.Count != 1) // TODO: Add test { - Log.Warning($"Symbol is null."); - return baseNode; + // Only port docs if there is only one variable in the declaration + return false; } - return VisitType(baseNode, symbol); + nodeWithSymbol = variableDecl.Variables.First(); } - - public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node) + else { - SyntaxNode? baseNode = base.VisitStructDeclaration(node); - - ISymbol? symbol = Model.GetDeclaredSymbol(node); - if (symbol == null) + // All members except enum values can have visibility modifiers + if (originalNode is not EnumMemberDeclarationSyntax && !IsPublic(originalNode)) { - Log.Warning($"Symbol is null."); - return baseNode; + return false; } - return VisitType(baseNode, symbol); + nodeWithSymbol = originalNode; } + - #endregion - - #region Visit helpers - - private SyntaxNode? VisitType(SyntaxNode? node, ISymbol? symbol) + if (Model.GetDeclaredSymbol(nodeWithSymbol) is ISymbol symbol) { - if (node == null || symbol == null) - { - return node; - } - string? docId = symbol.GetDocumentationCommentId(); - if (string.IsNullOrWhiteSpace(docId)) - { - Log.Warning($"DocId is null or empty."); - return node; - } - - SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - - if (!TryGetType(symbol, out DocsType? type)) - { - return node; - } - - SyntaxTriviaList leadingTrivia = node.GetLeadingTrivia(); - - SyntaxTriviaList summary = GetSummary(leadingTrivia, type, leadingWhitespace); - SyntaxTriviaList typeParameters = GetTypeParameters(leadingTrivia, type, leadingWhitespace); - SyntaxTriviaList parameters = GetParameters(leadingTrivia, type, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(leadingTrivia, type, leadingWhitespace); - SyntaxTriviaList seealsos = GetSeeAlsos(leadingTrivia, type.SeeAlsoCrefs, leadingWhitespace); - SyntaxTriviaList altmembers = GetAltMembers(leadingTrivia, type.AltMembers, leadingWhitespace); - SyntaxTriviaList relateds = GetRelateds(leadingTrivia, type.Relateds, leadingWhitespace); - - - return GetNodeWithTrivia(leadingWhitespace, node, summary, typeParameters, parameters, remarks, seealsos, altmembers, relateds); - } - - private SyntaxNode? VisitBaseMethodDeclaration(BaseMethodDeclarationSyntax node) - { - // The Docs files only contain docs for public elements, - // so if no comments are found, we return the node unmodified - if (!TryGetMember(node, out DocsMember? member)) - { - return node; - } - - SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList leadingTrivia = node.GetLeadingTrivia(); - - SyntaxTriviaList summary = GetSummary(leadingTrivia, member, leadingWhitespace); - SyntaxTriviaList typeParameters = GetTypeParameters(leadingTrivia, member, leadingWhitespace); - SyntaxTriviaList parameters = GetParameters(leadingTrivia, member, leadingWhitespace); - SyntaxTriviaList returns = GetReturns(leadingTrivia, member, leadingWhitespace); - SyntaxTriviaList exceptions = GetExceptions(leadingTrivia, member.Exceptions, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(leadingTrivia, member, leadingWhitespace); - SyntaxTriviaList seealsos = GetSeeAlsos(leadingTrivia, member.SeeAlsoCrefs, leadingWhitespace); - SyntaxTriviaList altmembers = GetAltMembers(leadingTrivia, member.AltMembers, leadingWhitespace); - SyntaxTriviaList relateds = GetRelateds(leadingTrivia, member.Relateds, leadingWhitespace); - - return GetNodeWithTrivia(leadingWhitespace, node, summary, typeParameters, parameters, returns, exceptions, remarks, seealsos, altmembers, relateds); - } - - private SyntaxNode? VisitMemberDeclaration(MemberDeclarationSyntax node) - { - if (!TryGetMember(node, out DocsMember? member)) + if (!string.IsNullOrWhiteSpace(docId)) { - return node; + DocsComments.Members.TryGetValue(docId, out member); } - - SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList leadingTrivia = node.GetLeadingTrivia(); - - SyntaxTriviaList summary = GetSummary(leadingTrivia, member, leadingWhitespace); - SyntaxTriviaList exceptions = GetExceptions(leadingTrivia, member.Exceptions, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(leadingTrivia, member, leadingWhitespace); - SyntaxTriviaList seealsos = GetSeeAlsos(leadingTrivia, member.SeeAlsoCrefs, leadingWhitespace); - SyntaxTriviaList altmembers = GetAltMembers(leadingTrivia, member.AltMembers, leadingWhitespace); - SyntaxTriviaList relateds = GetRelateds(leadingTrivia, member.Relateds, leadingWhitespace); - - return GetNodeWithTrivia(leadingWhitespace, node, summary, exceptions, remarks, seealsos, altmembers, relateds); } - private SyntaxNode? VisitVariableDeclaration(BaseFieldDeclarationSyntax node) - { - // The comments need to be extracted from the underlying variable declarator inside the declaration - VariableDeclarationSyntax declaration = node.Declaration; - - // Only port docs if there is only one variable in the declaration - if (declaration.Variables.Count == 1) - { - if (!TryGetMember(declaration.Variables.First(), out DocsMember? member)) - { - return node; - } - - SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList leadingTrivia = node.GetLeadingTrivia(); - - SyntaxTriviaList summary = GetSummary(leadingTrivia, member, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(leadingTrivia, member, leadingWhitespace); - SyntaxTriviaList seealsos = GetSeeAlsos(leadingTrivia, member.SeeAlsoCrefs, leadingWhitespace); - SyntaxTriviaList altmembers = GetAltMembers(leadingTrivia, member.AltMembers, leadingWhitespace); - SyntaxTriviaList relateds = GetRelateds(leadingTrivia, member.Relateds, leadingWhitespace); - - return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks, seealsos, altmembers, relateds); - } + return member != null; + } - return node; - } + private bool TryGetType(SyntaxNode originalNode, [NotNullWhen(returnValue: true)] out DocsType? type) + { + type = null; - private bool TryGetMember(SyntaxNode node, [NotNullWhen(returnValue: true)] out DocsMember? member) + if (originalNode == null || !IsPublic(originalNode)) { - member = null; - if (Model.GetDeclaredSymbol(node) is ISymbol symbol) - { - string? docId = symbol.GetDocumentationCommentId(); - if (!string.IsNullOrWhiteSpace(docId)) - { - DocsComments.Members.TryGetValue(docId, out member); - } - } - - return member != null; + return false; } - private bool TryGetType(ISymbol symbol, [NotNullWhen(returnValue: true)] out DocsType? type) + if (Model.GetDeclaredSymbol(originalNode) is ISymbol symbol) { - type = null; - string? docId = symbol.GetDocumentationCommentId(); if (!string.IsNullOrWhiteSpace(docId)) { DocsComments.Types.TryGetValue(docId, out type); } - - return type != null; - } - - #endregion - - #region Syntax manipulation - - private static SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxNode node, params SyntaxTriviaList[] trivias) - { - SyntaxTriviaList leadingDoubleSlashComments = GetLeadingDoubleSlashComments(node, leadingWhitespace); - - SyntaxTriviaList finalTrivia = new(); - foreach (SyntaxTriviaList t in trivias) - { - finalTrivia = finalTrivia.AddRange(t); - } - finalTrivia = finalTrivia.AddRange(leadingDoubleSlashComments); - - if (finalTrivia.Count > 0) - { - finalTrivia = finalTrivia.AddRange(leadingWhitespace); - - var leadingTrivia = node.GetLeadingTrivia(); - if (leadingTrivia.Any()) - { - if (leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) - { - // Ensure the endline that separates nodes is respected - finalTrivia = new SyntaxTriviaList(SyntaxFactory.ElasticLineFeed) - .AddRange(finalTrivia); - } - } - - return node.WithLeadingTrivia(finalTrivia); - } - - // If there was no new trivia, return untouched - return node; - } - - // Finds the last set of whitespace characters that are to the left of the public|protected keyword of the node. - private static SyntaxTriviaList GetLeadingWhitespace(SyntaxNode node) - { - SyntaxTriviaList triviaList = GetLeadingTrivia(node); - - if (triviaList.Any() && - triviaList.LastOrDefault(t => t.IsKind(SyntaxKind.WhitespaceTrivia)) is SyntaxTrivia last) - { - return new(last); - } - - return new(); - } - - private static SyntaxTriviaList GetLeadingDoubleSlashComments(SyntaxNode node, SyntaxTriviaList leadingWhitespace) - { - SyntaxTriviaList triviaList = GetLeadingTrivia(node); - - SyntaxTriviaList doubleSlashComments = new(); - - foreach (SyntaxTrivia trivia in triviaList) - { - if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia)) - { - doubleSlashComments = doubleSlashComments - .AddRange(leadingWhitespace) - .Add(trivia) - .Add(SyntaxFactory.LineFeed); - } - } - - return doubleSlashComments; - } - - private static SyntaxTriviaList GetLeadingTrivia(SyntaxNode node) - { - if (node is MemberDeclarationSyntax memberDeclaration) - { - if ((memberDeclaration.Modifiers.FirstOrDefault(x => x.IsKind(SyntaxKind.PublicKeyword) || x.IsKind(SyntaxKind.ProtectedKeyword)) is SyntaxToken modifier) && - !modifier.IsKind(SyntaxKind.None)) - { - return modifier.LeadingTrivia; - } - - return node.GetLeadingTrivia(); - } - - return new(); - } - - // Collects all tags with of the same name from a SyntaxTriviaList. - private static SyntaxTriviaList FindTag(string tag, SyntaxTriviaList leadingWhitespace, SyntaxTriviaList from) - { - List list = new(); - foreach(var trivia in from) - { - if (trivia.GetStructure() is DocumentationCommentTriviaSyntax structure) { - foreach(XmlNodeSyntax node in structure.Content) - { - if (node is XmlEmptyElementSyntax emptyElement && emptyElement.Name.ToString() == tag) { - list.Add(node); - } else if (node is XmlElementSyntax element && element.StartTag.Name.ToString() == tag) { - list.Add(node); - } - } - } - } - - return list.Any() ? GetXmlTrivia(leadingWhitespace, list.ToArray()) : new(); - } - - private static SyntaxTriviaList GetSummary(SyntaxTriviaList old, DocsAPI api, SyntaxTriviaList leadingWhitespace) - { - if (!api.Summary.IsDocsEmpty()) - { - XmlTextSyntax contents = GetTextAsCommentedTokens(api.Summary, leadingWhitespace); - XmlElementSyntax element = SyntaxFactory.XmlSummaryElement(contents); - return GetXmlTrivia(leadingWhitespace, element); - } - - return FindTag("summary", leadingWhitespace, old); - } - - private static SyntaxTriviaList GetRemarks(SyntaxTriviaList old, DocsAPI api, SyntaxTriviaList leadingWhitespace) - { - if (!api.Remarks.IsDocsEmpty()) - { - return GetFormattedRemarks(api, leadingWhitespace); - } - - return FindTag("remarks", leadingWhitespace, old); - } - - private static SyntaxTriviaList GetValue(SyntaxTriviaList old, DocsMember api, SyntaxTriviaList leadingWhitespace) - { - if (!api.Value.IsDocsEmpty()) - { - XmlTextSyntax contents = GetTextAsCommentedTokens(api.Value, leadingWhitespace); - XmlElementSyntax element = SyntaxFactory.XmlValueElement(contents); - return GetXmlTrivia(leadingWhitespace, element); - } - - return FindTag("value", leadingWhitespace, old); - } - - private static SyntaxTriviaList GetParameter(string name, string text, SyntaxTriviaList leadingWhitespace) - { - if (!text.IsDocsEmpty()) - { - XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace); - XmlElementSyntax element = SyntaxFactory.XmlParamElement(name, contents); - return GetXmlTrivia(leadingWhitespace, element); - } - - return new(); - } - - private static SyntaxTriviaList GetParameters(SyntaxTriviaList old, DocsAPI api, SyntaxTriviaList leadingWhitespace) - { - if (!api.Params.HasItems()) - { - return FindTag("param", leadingWhitespace, old); - } - SyntaxTriviaList parameters = new(); - foreach (SyntaxTriviaList parameterTrivia in api.Params - .Where(param => !param.Value.IsDocsEmpty()) - .Select(param => GetParameter(param.Name, param.Value, leadingWhitespace))) - { - parameters = parameters.AddRange(parameterTrivia); - } - return parameters; - } - - private static SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace) - { - if (!text.IsDocsEmpty()) - { - var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute("name", name)); - XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace); - return GetXmlTrivia("typeparam", attribute, contents, leadingWhitespace); - } - - return new(); } - private static SyntaxTriviaList GetTypeParameters(SyntaxTriviaList old, DocsAPI api, SyntaxTriviaList leadingWhitespace) - { - if (!api.TypeParams.HasItems()) - { - return FindTag("typeparams", leadingWhitespace, old); - } - SyntaxTriviaList typeParameters = new(); - foreach (SyntaxTriviaList typeParameterTrivia in api.TypeParams - .Where(typeParam => !typeParam.Value.IsDocsEmpty()) - .Select(typeParam => GetTypeParam(typeParam.Name, typeParam.Value, leadingWhitespace))) - { - typeParameters = typeParameters.AddRange(typeParameterTrivia); - } - return typeParameters; - } - - private static SyntaxTriviaList GetReturns(SyntaxTriviaList old, DocsMember api, SyntaxTriviaList leadingWhitespace) - { - // Also applies for when is empty because the method return type is void - if (!api.Returns.IsDocsEmpty()) - { - XmlTextSyntax contents = GetTextAsCommentedTokens(api.Returns, leadingWhitespace); - XmlElementSyntax element = SyntaxFactory.XmlReturnsElement(contents); - return GetXmlTrivia(leadingWhitespace, element); - } - - return FindTag("returns", leadingWhitespace, old); - } - - private static SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace) - { - if (!text.IsDocsEmpty()) - { - cref = RemoveCrefPrefix(cref); - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); - XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace); - XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); - return GetXmlTrivia(leadingWhitespace, element); - } - - return new(); - } - - private static SyntaxTriviaList GetExceptions(SyntaxTriviaList old, List docsExceptions, SyntaxTriviaList leadingWhitespace) - { - if (!docsExceptions.Any()) - { - return FindTag("exception", leadingWhitespace, old); - } - SyntaxTriviaList exceptions = new(); - foreach (SyntaxTriviaList exceptionsTrivia in docsExceptions.Select( - exception => GetException(exception.Cref, exception.Value, leadingWhitespace))) - { - exceptions = exceptions.AddRange(exceptionsTrivia); - } - return exceptions; - } - - private static SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace) - { - cref = RemoveCrefPrefix(cref); - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); - XmlEmptyElementSyntax element = SyntaxFactory.XmlSeeAlsoElement(crefSyntax); - return GetXmlTrivia(leadingWhitespace, element); - } - - private static SyntaxTriviaList GetSeeAlsos(SyntaxTriviaList old, List docsSeeAlsoCrefs, SyntaxTriviaList leadingWhitespace) - { - if (!docsSeeAlsoCrefs.Any()) - { - return FindTag("seealso", leadingWhitespace, old); - } - SyntaxTriviaList seealsos = new(); - foreach (SyntaxTriviaList seealsoTrivia in docsSeeAlsoCrefs.Select( - s => GetSeeAlso(s, leadingWhitespace))) - { - seealsos = seealsos.AddRange(seealsoTrivia); - } - return seealsos; - } - - private static SyntaxTriviaList GetAltMember(string cref, SyntaxTriviaList leadingWhitespace) - { - cref = RemoveCrefPrefix(cref); - XmlAttributeSyntax attribute = SyntaxFactory.XmlTextAttribute("cref", cref); - XmlEmptyElementSyntax emptyElement = SyntaxFactory.XmlEmptyElement(SyntaxFactory.XmlName(SyntaxFactory.Identifier("altmember")), new SyntaxList(attribute)); - return GetXmlTrivia(leadingWhitespace, emptyElement); - } + return type != null; + } - private static SyntaxTriviaList GetAltMembers(SyntaxTriviaList old, List docsAltMembers, SyntaxTriviaList leadingWhitespace) - { - if (!docsAltMembers.Any()) - { - return FindTag("altmember", leadingWhitespace, old); - } - SyntaxTriviaList altMembers = new(); - foreach (SyntaxTriviaList altMemberTrivia in docsAltMembers.Select( - s => GetAltMember(s, leadingWhitespace))) - { - altMembers = altMembers.AddRange(altMemberTrivia); - } - return altMembers; - } + private static bool IsPublic([NotNullWhen(returnValue: true)] SyntaxNode? node) => + node != null && + node is MemberDeclarationSyntax baseNode && + baseNode.Modifiers.Any(t => t.IsKind(SyntaxKind.PublicKeyword)); - private static SyntaxTriviaList GetRelated(string articleType, string href, string value, SyntaxTriviaList leadingWhitespace) - { - SyntaxList attributes = new(); + public SyntaxNode Generate(SyntaxNode node, IDocsAPI api) + { + List updatedLeadingTrivia = new(); - attributes = attributes.Add(SyntaxFactory.XmlTextAttribute("type", articleType)); - attributes = attributes.Add(SyntaxFactory.XmlTextAttribute("href", href)); + bool replacedExisting = false; + SyntaxTriviaList leadingTrivia = node.GetLeadingTrivia(); - XmlTextSyntax contents = GetTextAsCommentedTokens(value, leadingWhitespace); - return GetXmlTrivia("related", attributes, contents, leadingWhitespace); - } + SyntaxTrivia? indentationTrivia = leadingTrivia.Count > 0 ? leadingTrivia.Last(x => x.IsKind(SyntaxKind.WhitespaceTrivia)) : null; - private static SyntaxTriviaList GetRelateds(SyntaxTriviaList old, List docsRelateds, SyntaxTriviaList leadingWhitespace) - { - if (!docsRelateds.Any()) - { - return FindTag("related", leadingWhitespace, old); - } - SyntaxTriviaList relateds = new(); - foreach (SyntaxTriviaList relatedsTrivia in docsRelateds.Select( - s => GetRelated(s.ArticleType, s.Href, s.Value, leadingWhitespace))) - { - relateds = relateds.AddRange(relatedsTrivia); - } - return relateds; - } + DocumentationUpdater updater = new(DocsComments.Config, api, indentationTrivia); - private static XmlTextSyntax GetTextAsCommentedTokens(string text, SyntaxTriviaList leadingWhitespace, bool wrapWithNewLines = false) + for (int index = 0; index < leadingTrivia.Count; index++) { - text = CleanCrefs(text); - - // collapse newlines to a single one - string whitespace = Regex.Replace(leadingWhitespace.ToFullString(), @"(\r?\n)+", ""); - SyntaxToken whitespaceToken = SyntaxFactory.XmlTextNewLine(UnixNewLine + whitespace); - - SyntaxTrivia leadingTrivia = SyntaxFactory.SyntaxTrivia(SyntaxKind.DocumentationCommentExteriorTrivia, string.Empty); - SyntaxTriviaList leading = SyntaxTriviaList.Create(leadingTrivia); - - string[] lines = text.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - - var tokens = new List(); - - if (wrapWithNewLines) - { - tokens.Add(whitespaceToken); - } + SyntaxTrivia originalTrivia = leadingTrivia[index]; - for (int lineNumber = 0; lineNumber < lines.Length; lineNumber++) + if (index == leadingTrivia.Count - 1) { - string line = lines[lineNumber]; - - SyntaxToken token = SyntaxFactory.XmlTextLiteral(leading, line, line, default); - tokens.Add(token); - - if (lines.Length > 1 && lineNumber < lines.Length - 1) - { - tokens.Add(whitespaceToken); - } + // Skip the last one because it will be added at the end + break; } - if (wrapWithNewLines) + if (originalTrivia.IsKind(SyntaxKind.WhitespaceTrivia)) { - tokens.Add(whitespaceToken); + // Avoid re-adding existing whitespace trivia, it will always be added later + continue; } - XmlTextSyntax xmlText = SyntaxFactory.XmlText(tokens.ToArray()); - return xmlText; - } - - private static SyntaxTriviaList GetXmlTrivia(SyntaxTriviaList leadingWhitespace, params XmlNodeSyntax[] nodes) - { - DocumentationCommentTriviaSyntax docComment = SyntaxFactory.DocumentationComment(nodes); - SyntaxTrivia docCommentTrivia = SyntaxFactory.Trivia(docComment); - - return leadingWhitespace - .Add(docCommentTrivia) - .Add(SyntaxFactory.LineFeed); - } - - // Generates a custom SyntaxTrivia object containing a triple slashed xml element with optional attributes. - // Looks like below (excluding square brackets): - // [ /// text] - private static SyntaxTriviaList GetXmlTrivia(string name, SyntaxList attributes, XmlTextSyntax contents, SyntaxTriviaList leadingWhitespace) - { - XmlElementStartTagSyntax start = SyntaxFactory.XmlElementStartTag( - SyntaxFactory.Token(SyntaxKind.LessThanToken), - SyntaxFactory.XmlName(SyntaxFactory.Identifier(name)), - attributes, - SyntaxFactory.Token(SyntaxKind.GreaterThanToken)); - - XmlElementEndTagSyntax end = SyntaxFactory.XmlElementEndTag( - SyntaxFactory.Token(SyntaxKind.LessThanSlashToken), - SyntaxFactory.XmlName(SyntaxFactory.Identifier(name)), - SyntaxFactory.Token(SyntaxKind.GreaterThanToken)); - - XmlElementSyntax element = SyntaxFactory.XmlElement(start, new SyntaxList(contents), end); - return GetXmlTrivia(leadingWhitespace, element); - } - - private static string WrapInRemarks(string acum) - { - string wrapped = UnixNewLine + "" + UnixNewLine; - return wrapped; - } - - private static string WrapCodeIncludes(string[] splitted, ref int n) - { - string acum = string.Empty; - while (n < splitted.Length && splitted[n].ContainsStrings(MarkdownCodeIncludes)) + if (!originalTrivia.HasStructure) { - acum += UnixNewLine + splitted[n]; - if ((n + 1) < splitted.Length && splitted[n + 1].ContainsStrings(MarkdownCodeIncludes)) + // Double slash comments do not have a structure but must be preserved with the original indentation + // Only add indentation if the current trivia is not a new line + if ((SyntaxKind)originalTrivia.RawKind != SyntaxKind.EndOfLineTrivia && indentationTrivia.HasValue) { - n++; - } - else - { - break; + updatedLeadingTrivia.Add(indentationTrivia.Value); } + updatedLeadingTrivia.Add(originalTrivia); + + continue; } - return WrapInRemarks(acum); - } - private static SyntaxTriviaList GetFormattedRemarks(IDocsAPI api, SyntaxTriviaList leadingWhitespace) - { + SyntaxNode? structuredTrivia = originalTrivia.GetStructure(); + Debug.Assert(structuredTrivia != null); - string remarks = RemoveUnnecessaryMarkdown(api.Remarks); - string example = string.Empty; - - XmlNodeSyntax contents; - if (remarks.ContainsStrings(MarkdownUnconvertableStrings)) - { - contents = GetTextAsFormatCData(remarks, leadingWhitespace); - } - else + if (!structuredTrivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia)) { - string[] splitted = remarks.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - string updatedRemarks = string.Empty; - for (int n = 0; n < splitted.Length; n++) + // Unsure if there are other structured comments, but must preserve them with the original indentation + if (indentationTrivia.HasValue) { - string acum; - string line = splitted[n]; - if (line.ContainsStrings(MarkdownHeaders)) - { - acum = line; - n++; - while (n < splitted.Length && splitted[n].StartsWith(">")) - { - acum += UnixNewLine + splitted[n]; - if ((n + 1) < splitted.Length && splitted[n + 1].StartsWith(">")) - { - n++; - } - else - { - break; - } - } - updatedRemarks += WrapInRemarks(acum); - } - else if (line.ContainsStrings(MarkdownCodeIncludes)) - { - updatedRemarks += WrapCodeIncludes(splitted, ref n); - } - // When an example is found, everything after the header is considered part of that section - else if (line.Contains("## Example")) - { - n++; - while (n < splitted.Length) - { - line = splitted[n]; - if (line.ContainsStrings(MarkdownCodeIncludes)) - { - example += WrapCodeIncludes(splitted, ref n); - } - else - { - example += UnixNewLine + line; - } - n++; - } - } - else - { - updatedRemarks += ReplaceMarkdownWithXmlElements(UnixNewLine + line, api.Params, api.TypeParams); - } + updatedLeadingTrivia.Add(indentationTrivia.Value); } - - contents = GetTextAsCommentedTokens(updatedRemarks, leadingWhitespace); - } - - XmlElementSyntax remarksXml = SyntaxFactory.XmlRemarksElement(contents); - SyntaxTriviaList result = GetXmlTrivia(leadingWhitespace, remarksXml); - - if (!string.IsNullOrWhiteSpace(example)) - { - SyntaxTriviaList exampleTriviaList = GetFormattedExamples(api, example, leadingWhitespace); - result = result.AddRange(exampleTriviaList); + updatedLeadingTrivia.Add(originalTrivia); + continue; } - return result; - } - - private static SyntaxTriviaList GetFormattedExamples(IDocsAPI api, string example, SyntaxTriviaList leadingWhitespace) - { - example = ReplaceMarkdownWithXmlElements(example, api.Params, api.TypeParams); - XmlNodeSyntax exampleContents = GetTextAsCommentedTokens(example, leadingWhitespace); - XmlElementSyntax exampleXml = SyntaxFactory.XmlExampleElement(exampleContents); - SyntaxTriviaList exampleTriviaList = GetXmlTrivia(leadingWhitespace, exampleXml); - return exampleTriviaList; - } - - private static XmlNodeSyntax GetTextAsFormatCData(string text, SyntaxTriviaList leadingWhitespace) - { - XmlTextSyntax remarks = GetTextAsCommentedTokens(text, leadingWhitespace, wrapWithNewLines: true); - - XmlNameSyntax formatName = SyntaxFactory.XmlName("format"); - XmlAttributeSyntax formatAttribute = SyntaxFactory.XmlTextAttribute("type", "text/markdown"); - var formatAttributes = new SyntaxList(formatAttribute); - - var formatStart = SyntaxFactory.XmlElementStartTag(formatName, formatAttributes); - var formatEnd = SyntaxFactory.XmlElementEndTag(formatName); - - XmlCDataSectionSyntax cdata = SyntaxFactory.XmlCDataSection(remarks.TextTokens); - var cdataList = new SyntaxList(cdata); + // We know there is at least one xml element + SyntaxList existingDocs = ((DocumentationCommentTriviaSyntax)structuredTrivia).Content; + SyntaxTriviaList triviaList = SyntaxFactory.TriviaList(SyntaxFactory.Trivia(updater.GetUpdatedDocs(existingDocs))); + updatedLeadingTrivia.AddRange(triviaList); - XmlElementSyntax contents = SyntaxFactory.XmlElement(formatStart, cdataList, formatEnd); - - return contents; - } - - private static string RemoveUnnecessaryMarkdown(string text) - { - text = Regex.Replace(text, @"", ""); - text = Regex.Replace(text, @"##[ ]?Remarks(\r?\n)*[\t ]*", ""); - return text; - } - - private static string ReplaceMarkdownWithXmlElements(string text, List docsParams, List docsTypeParams) - { - text = CleanXrefs(text); - - // commonly used url entities - text = Regex.Replace(text, @"%23", "#"); - text = Regex.Replace(text, @"%28", "("); - text = Regex.Replace(text, @"%29", ")"); - text = Regex.Replace(text, @"%2C", ","); - - // hyperlinks - text = Regex.Replace(text, RegexMarkdownLinkPattern, RegexHtmlLinkReplacement); - - // bold - text = Regex.Replace(text, RegexMarkdownBoldPattern, RegexXmlBoldReplacement); - - // code snippet - text = Regex.Replace(text, RegexMarkdownCodeStartPattern, RegexXmlCodeStartReplacement); - text = Regex.Replace(text, RegexMarkdownCodeEndPattern, RegexXmlCodeEndReplacement); - - // langwords|parameters|typeparams - MatchCollection collection = Regex.Matches(text, @"(?`(?[a-zA-Z0-9_]+)`)"); - foreach (Match match in collection) - { - string backtickedParam = match.Groups["backtickedParam"].Value; - string paramName = match.Groups["paramName"].Value; - if (ReservedKeywords.Any(x => x == paramName)) - { - text = Regex.Replace(text, $"{backtickedParam}", $""); - } - else if (docsParams.Any(x => x.Name == paramName)) - { - text = Regex.Replace(text, $"{backtickedParam}", $""); - } - else if (docsTypeParams.Any(x => x.Name == paramName)) - { - text = Regex.Replace(text, $"{backtickedParam}", $""); - } - } - - return text; - } - - // Removes the one letter prefix and the following colon, if found, from a cref. - private static string RemoveCrefPrefix(string cref) - { - if (cref.Length > 2 && cref[1] == ':') - { - return cref[2..]; - } - return cref; - } - - private static string ReplacePrimitives(string text) - { - foreach ((string key, string value) in PrimitiveTypes) - { - text = Regex.Replace(text, key, value); - } - return text; - } - - private static string ReplaceDocId(Match m) - { - string docId = m.Groups["docId"].Value; - string overload = string.IsNullOrWhiteSpace(m.Groups["overload"].Value) ? "" : "O:"; - docId = ReplacePrimitives(docId); - docId = Regex.Replace(docId, @"%60", "`"); - docId = Regex.Replace(docId, @"`\d", "{T}"); - return overload + docId; - } - - private static string CrefEvaluator(Match m) - { - string docId = ReplaceDocId(m); - return "cref=\"" + docId + "\""; - } - - private static string CleanCrefs(string text) - { - text = Regex.Replace(text, RegexXmlCrefPattern, CrefEvaluator); - return text; + replacedExisting = true; } - private static string XrefEvaluator(Match m) + // Either there was no pre-existing trivia or there were no + // existing triple slash, so it must be built from scratch + if (!replacedExisting) { - string docId = ReplaceDocId(m); - return ""; + SyntaxTriviaList triviaList = SyntaxFactory.TriviaList(SyntaxFactory.Trivia(updater.GetNewDocs())); + updatedLeadingTrivia.AddRange(triviaList); } - private static string CleanXrefs(string text) + // The last trivia is the spacing before the actual node (usually before the visibility keyword) + // must be replaced in its original location + if (indentationTrivia.HasValue) { - text = Regex.Replace(text, RegexMarkdownXrefPattern, XrefEvaluator); - return text; + updatedLeadingTrivia.Add(indentationTrivia.Value); } - #endregion + return node.WithLeadingTrivia(updatedLeadingTrivia); } } diff --git a/src/PortToTripleSlash/src/libraries/ToTripleSlashPorter.cs b/src/PortToTripleSlash/src/libraries/ToTripleSlashPorter.cs index 790654e..c15f522 100644 --- a/src/PortToTripleSlash/src/libraries/ToTripleSlashPorter.cs +++ b/src/PortToTripleSlash/src/libraries/ToTripleSlashPorter.cs @@ -96,7 +96,9 @@ public async Task StartAsync(CancellationToken cancellationToken) Log.Error("No docs files found."); return; } + await MatchSymbolsAsync(_config.Loader.MainProject.Compilation, isMSBuildProject: true, cancellationToken).ConfigureAwait(false); + await PortAsync(isMSBuildProject: true, cancellationToken).ConfigureAwait(false); } @@ -144,7 +146,7 @@ public async Task PortAsync(bool isMSBuildProject, CancellationToken cancellatio foreach (ResolvedLocation resolvedLocation in docsType.SymbolLocations) { Log.Info($"Porting docs for tree '{resolvedLocation.Tree.FilePath}'..."); - TripleSlashSyntaxRewriter rewriter = new(_docsComments, resolvedLocation.Model); + TripleSlashSyntaxRewriter rewriter = new(_docsComments, resolvedLocation); SyntaxNode root = resolvedLocation.Tree.GetRoot(cancellationToken); resolvedLocation.NewNode = rewriter.Visit(root); if (resolvedLocation.NewNode == null) @@ -250,7 +252,6 @@ private static void FindLocationsOfSymbolInResolvedProject(DocsType docsType, Co // Next, filter types that match the current docsType IEnumerable currentTypeSymbols = visitor.AllTypesSymbols.Where(s => s != null && s.GetDocumentationCommentId() == docsType.DocId); - docsType.SymbolLocations ??= new(); foreach (ISymbol symbol in currentTypeSymbols) { GetSymbolLocations(docsType.SymbolLocations, compilation, symbol); diff --git a/src/PortToTripleSlash/src/libraries/XmlHelper.cs b/src/PortToTripleSlash/src/libraries/XmlHelper.cs index f404c11..2bf8933 100644 --- a/src/PortToTripleSlash/src/libraries/XmlHelper.cs +++ b/src/PortToTripleSlash/src/libraries/XmlHelper.cs @@ -1,8 +1,9 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; +using System.Linq; +using System.Text; using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; @@ -11,78 +12,15 @@ namespace ApiDocsSync.PortToTripleSlash { internal class XmlHelper { - private static readonly Dictionary _replaceableNormalElementPatterns = new Dictionary { - { "null", ""}, - { "true", ""}, - { "false", ""}, - { " null ", " " }, - { " true ", " " }, - { " false ", " " }, - { " null,", " ," }, - { " true,", " ," }, - { " false,", " ," }, - { " null.", " ." }, - { " true.", " ." }, - { " false.", " ." }, - { "null ", " " }, - { "true ", " " }, - { "false ", " " }, - { "Null ", " " }, - { "True ", " " }, - { "False ", " " }, - { ">", " />" } - }; - - private static readonly Dictionary _replaceableMarkdownPatterns = new Dictionary { - { "", "`null`" }, - { "", "`null`" }, - { "", "`true`" }, - { "", "`true`" }, - { "", "`false`" }, - { "", "`false`" }, - { "", "`"}, - { "", "`"}, - { "", "" }, - { "", "\r\n\r\n" }, - { "\" />", ">" }, - { "", "" }, - { "", ""}, - { "", "" } - }; - - private static readonly Dictionary _replaceableExceptionPatterns = new Dictionary{ - - { "", "\r\n" }, - { "", "" } - }; - - private static readonly Dictionary _replaceableMarkdownRegexPatterns = new Dictionary { - { @"\", @"`${paramrefContents}`" }, - { @"\", @"seealsoContents" }, + private static readonly (string, string)[] ReplaceableMarkdownPatterns = new[] + { + (@"\s*\s*", ""), + (@"\s*##\s*Remarks\s*", ""), + (@"`(?'keyword'null|false|true)`", ""), + (@"(?'keyword'null|false|true)", ""), + (@"\?\,]+)>", ""), + (@"%601", "{T}") }; public static string GetAttributeValue(XElement parent, string name) @@ -120,201 +58,52 @@ public static string GetChildElementValue(XElement parent, string childName) if (child != null) { - return GetNodesInPlainText(child); + return GetNodesInPlainText(childName, child); } return string.Empty; } - public static string GetNodesInPlainText(XElement element) + public static string GetNodesInPlainText(string name, XElement element) { if (element == null) { throw new Exception("A null element was passed when attempting to retrieve the nodes in plain text."); } + if (name == "remarks") + { + XElement? formatElement = element.Element("format"); + if (formatElement != null) + { + element = formatElement; + } + } // string.Join("", element.Nodes()) is very slow. // // The following is twice as fast (although still slow) // but does not produce the same spacing. That may be OK. // - //using var reader = element.CreateReader(); - //reader.MoveToContent(); - //return reader.ReadInnerXml().Trim(); - - return string.Join("", element.Nodes()).Trim(); - } - - public static void SaveFormattedAsMarkdown(XElement element, string newValue, bool isMember) - { - if (element == null) - { - throw new Exception("A null element was passed when attempting to save formatted as markdown"); - } - - // Empty value because SaveChildElement will add a child to the parent, not replace it - element.Value = string.Empty; - - XElement xeFormat = new XElement("format"); - - string updatedValue = SubstituteRemarksRegexPatterns(newValue); - updatedValue = ReplaceMarkdownPatterns(updatedValue).Trim(); - - string remarksTitle = string.Empty; - if (!updatedValue.Contains("## Remarks")) - { - remarksTitle = "## Remarks\r\n\r\n"; - } - - string spaces = isMember ? " " : " "; - - xeFormat.ReplaceAll(new XCData("\r\n\r\n" + remarksTitle + updatedValue + "\r\n\r\n" + spaces)); - - // Attribute at the end, otherwise it would be replaced by ReplaceAll - xeFormat.SetAttributeValue("type", "text/markdown"); - - element.Add(xeFormat); - } - - public static void AddChildFormattedAsMarkdown(XElement parent, XElement child, string childValue, bool isMember) - { - if (parent == null) - { - throw new Exception("A null parent was passed when attempting to add child formatted as markdown."); - } - - if (child == null) - { - throw new Exception("A null child was passed when attempting to add child formatted as markdown."); - } - - SaveFormattedAsMarkdown(child, childValue, isMember); - parent.Add(child); - } - - public static void SaveFormattedAsXml(XElement element, string newValue, bool removeUndesiredEndlines = true) - { - if (element == null) - { - throw new Exception("A null element was passed when attempting to save formatted as xml"); - } - - element.Value = string.Empty; - - var attributes = element.Attributes(); - - string updatedValue = removeUndesiredEndlines ? RemoveUndesiredEndlines(newValue) : newValue; - updatedValue = ReplaceNormalElementPatterns(updatedValue); - - // Workaround: will ensure XElement does not complain about having an invalid xml object inside. Those tags will be removed by replacing the nodes. - XElement parsedElement; - try - { - parsedElement = XElement.Parse("" + updatedValue + ""); - } - catch (XmlException) - { - parsedElement = XElement.Parse("" + updatedValue.Replace("<", "<").Replace(">", ">") + ""); - } - - element.ReplaceNodes(parsedElement.Nodes()); - - // Ensure attributes are preserved after replacing nodes - element.ReplaceAttributes(attributes); - } - - public static void AppendFormattedAsXml(XElement element, string valueToAppend, bool removeUndesiredEndlines) - { - if (element == null) - { - throw new Exception("A null element was passed when attempting to append formatted as xml"); - } - - SaveFormattedAsXml(element, GetNodesInPlainText(element) + valueToAppend, removeUndesiredEndlines); - } - - public static void AddChildFormattedAsXml(XElement parent, XElement child, string childValue) - { - if (parent == null) - { - throw new Exception("A null parent was passed when attempting to add child formatted as xml"); - } - - if (child == null) - { - throw new Exception("A null child was passed when attempting to add child formatted as xml"); - } - - SaveFormattedAsXml(child, childValue); - parent.Add(child); - } - - private static string RemoveUndesiredEndlines(string value) - { - value = Regex.Replace(value, @"((?'undesiredEndlinePrefix'[^\.\:])(\r\n)+[ \t]*)", @"${undesiredEndlinePrefix} "); + using XmlReader reader = element.CreateReader(); + reader.MoveToContent(); + string actualValue = reader.ReadInnerXml().Trim(); - return value.Trim(); - } - - private static string SubstituteRemarksRegexPatterns(string value) - { - return SubstituteRegexPatterns(value, _replaceableMarkdownRegexPatterns); - } - - private static string ReplaceMarkdownPatterns(string value) - { - string updatedValue = value; - foreach (KeyValuePair kvp in _replaceableMarkdownPatterns) + if (name == "remarks") { - if (updatedValue.Contains(kvp.Key)) - { - updatedValue = updatedValue.Replace(kvp.Key, kvp.Value); - } + actualValue = ReplaceMarkdown(actualValue); } - return updatedValue; - } - internal static string ReplaceExceptionPatterns(string value) - { - string updatedValue = value; - foreach (KeyValuePair kvp in _replaceableExceptionPatterns) - { - if (updatedValue.Contains(kvp.Key)) - { - updatedValue = updatedValue.Replace(kvp.Key, kvp.Value); - } - } - - updatedValue = Regex.Replace(updatedValue, @"[\r\n\t ]+\-[ ]?or[ ]?\-[\r\n\t ]+", "\r\n\r\n-or-\r\n\r\n"); - return updatedValue; - } - - private static string ReplaceNormalElementPatterns(string value) - { - string updatedValue = value; - foreach (KeyValuePair kvp in _replaceableNormalElementPatterns) - { - if (updatedValue.Contains(kvp.Key)) - { - updatedValue = updatedValue.Replace(kvp.Key, kvp.Value); - } - } - - return updatedValue; + return actualValue.IsDocsEmpty() ? string.Empty : actualValue; } - private static string SubstituteRegexPatterns(string value, Dictionary replaceableRegexPatterns) + private static string ReplaceMarkdown(string value) { - foreach (KeyValuePair pattern in replaceableRegexPatterns) + foreach ((string bad, string good) in ReplaceableMarkdownPatterns) { - Regex regex = new Regex(pattern.Key); - if (regex.IsMatch(value)) - { - value = regex.Replace(value, pattern.Value); - } + value = Regex.Replace(value, bad, good); } - return value; + return string.Join(Environment.NewLine, value.Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); } } } diff --git a/src/PortToTripleSlash/tests/PortToTripleSlash/PortToTripleSlash.FileSystem.Tests.cs b/src/PortToTripleSlash/tests/PortToTripleSlash/PortToTripleSlash.FileSystem.Tests.cs index b1fa237..f0d54e2 100644 --- a/src/PortToTripleSlash/tests/PortToTripleSlash/PortToTripleSlash.FileSystem.Tests.cs +++ b/src/PortToTripleSlash/tests/PortToTripleSlash/PortToTripleSlash.FileSystem.Tests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.IO; @@ -16,11 +16,9 @@ public PortToTripleSlash_FileSystem_Tests(ITestOutputHelper output) : base(outpu { } - [Fact] - public Task Port_Basic() => PortToTripleSlashAsync("Basic"); - - [Fact] - public Task Port_Generics() => PortToTripleSlashAsync("Generics"); + //[Fact] + // TODO: Need to fix the remark conversion from markdown to xml. + private Task Port_Basic() => PortToTripleSlashAsync("Basic"); private static async Task PortToTripleSlashAsync( string testDataDir, @@ -41,6 +39,7 @@ private static async Task PortToTripleSlashAsync( CsProj = Path.GetFullPath(testData.ProjectFilePath), SkipInterfaceImplementations = skipInterfaceImplementations, BinLogPath = testData.BinLogPath, + SkipRemarks = false }; c.IncludedAssemblies.Add(assemblyName); diff --git a/src/PortToTripleSlash/tests/PortToTripleSlash/PortToTripleSlash.Strings.Tests.cs b/src/PortToTripleSlash/tests/PortToTripleSlash/PortToTripleSlash.Strings.Tests.cs index 58182c5..6e08b23 100644 --- a/src/PortToTripleSlash/tests/PortToTripleSlash/PortToTripleSlash.Strings.Tests.cs +++ b/src/PortToTripleSlash/tests/PortToTripleSlash/PortToTripleSlash.Strings.Tests.cs @@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Newtonsoft.Json.Linq; using Xunit; using Xunit.Abstractions; @@ -24,8 +25,10 @@ public PortToTripleSlash_Strings_Tests(ITestOutputHelper output) : base(output) { } - [Fact] - public Task Class_TypeDescription() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_TypeDescription(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -47,23 +50,27 @@ public class MyClass { }"; - string expectedCode = @"namespace MyNamespace; -/// This is the MyClass summary. -/// These are the MyClass remarks. -public class MyClass + string expectedCode = $@"namespace MyNamespace; +/// +/// This is the MyClass summary. +/// " + +GetRemarks(skipRemarks, "MyClass") + +@"public class MyClass { }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Struct_TypeDescription() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Struct_TypeDescription(bool skipRemarks) { string docId = "T:MyNamespace.MyStruct"; @@ -86,22 +93,26 @@ public struct MyStruct }"; string expectedCode = @"namespace MyNamespace; -/// This is the MyStruct summary. -/// These are the MyStruct remarks. -public struct MyStruct +/// +/// This is the MyStruct summary. +/// " + +GetRemarks(skipRemarks, "MyStruct") + +@"public struct MyStruct { }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Interface_TypeDescription() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Interface_TypeDescription(bool skipRemarks) { string docId = "T:MyNamespace.MyInterface"; @@ -124,22 +135,26 @@ public interface MyInterface }"; string expectedCode = @"namespace MyNamespace; -/// This is the MyInterface summary. -/// These are the MyInterface remarks. -public interface MyInterface +/// +/// This is the MyInterface summary. +/// " + +GetRemarks(skipRemarks, "MyInterface") + +@"public interface MyInterface { }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Enum_TypeDescription() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Enum_TypeDescription(bool skipRemarks) { string docId = "T:MyNamespace.MyEnum"; @@ -162,22 +177,26 @@ public enum MyEnum }"; string expectedCode = @"namespace MyNamespace; -/// This is the MyEnum summary. -/// These are the MyEnum remarks. -public enum MyEnum +/// +/// This is the MyEnum summary. +/// " + +GetRemarks(skipRemarks, "MyEnum") + +@"public enum MyEnum { }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_Ctor_Parameterless() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_Ctor_Parameterless(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -210,23 +229,26 @@ public MyClass() { } string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the MyClass constructor summary. - /// These are the MyClass constructor remarks. - public MyClass() { } + /// + /// This is the MyClass constructor summary. + /// " + +GetRemarks(skipRemarks, "MyClass constructor", " ") + +@" public MyClass() { } }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_Ctor_IntParameter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_Ctor_IntParameter(bool skipRemarks) { - string docId = "T:MyNamespace.MyClass"; string docFile = @" @@ -259,22 +281,26 @@ public MyClass(int intParam) { } string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the MyClass constructor summary. - /// This is the MyClass constructor parameter description. - /// These are the MyClass constructor remarks. - public MyClass(int intParam) { } + /// + /// This is the MyClass constructor summary. + /// + /// This is the MyClass constructor parameter description." + +GetRemarks(skipRemarks, "MyClass constructor", " ") + +@" public MyClass(int intParam) { } }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_Method_Parameterless_VoidReturn() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_Method_Parameterless_VoidReturn(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -307,21 +333,25 @@ public void MyVoidMethod() { } string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the MyVoidMethod summary. - /// These are the MyVoidMethod remarks. - public void MyVoidMethod() { } + /// + /// This is the MyVoidMethod summary. + /// " + +GetRemarks(skipRemarks, "MyVoidMethod", " ") + +@" public void MyVoidMethod() { } }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_Method_IntParameter_IntReturn() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_Method_IntParameter_IntReturn(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -356,23 +386,27 @@ public class MyClass string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the MyIntMethod summary. + /// + /// This is the MyIntMethod summary. + /// /// This is the MyIntMethod withArgument description. - /// This is the MyIntMethod returns description. - /// These are the MyIntMethod remarks. - public int MyIntMethod(int withArgument) => withArgument; + /// This is the MyIntMethod returns description." + +GetRemarks(skipRemarks, "MyIntMethod", " ") + +@" public int MyIntMethod(int withArgument) => withArgument; }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_GenericMethod_Parameterless_VoidReturn() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_GenericMethod_Parameterless_VoidReturn(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -406,22 +440,26 @@ public void MyGenericMethod() { } string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the MyGenericMethod summary. - /// This is the MyGenericMethod type parameter description. - /// These are the MyGenericMethod remarks. - public void MyGenericMethod() { } + /// + /// This is the MyGenericMethod summary. + /// + /// This is the MyGenericMethod type parameter description." + +GetRemarks(skipRemarks, "MyGenericMethod", " ") + +@" public void MyGenericMethod() { } }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_GenericMethod_IntParameter_VoidReturn() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_GenericMethod_IntParameter_VoidReturn(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -456,23 +494,27 @@ public void MyGenericMethod(int intParam) { } string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the MyGenericMethod summary. + /// + /// This is the MyGenericMethod summary. + /// /// This is the MyGenericMethod type parameter description. - /// This is the MyGenericMethod parameter description. - /// These are the MyGenericMethod remarks. - public void MyGenericMethod(int intParam) { } + /// This is the MyGenericMethod parameter description." + +GetRemarks(skipRemarks, "MyGenericMethod", " ") + +@" public void MyGenericMethod(int intParam) { } }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_GenericMethod_GenericParameter_GenericReturn() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_GenericMethod_GenericParameter_GenericReturn(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -508,24 +550,28 @@ public class MyClass string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the MyGenericMethod summary. + /// + /// This is the MyGenericMethod summary. + /// /// This is the MyGenericMethod type parameter description. /// This is the MyGenericMethod withGenericArgument description. - /// This is the MyGenericMethod returns description. - /// These are the MyGenericMethod remarks. - public T MyGenericMethod(T withGenericArgument) => withGenericArgument; + /// This is the MyGenericMethod returns description." + +GetRemarks(skipRemarks, "MyGenericMethod", " ") + +@" public T MyGenericMethod(T withGenericArgument) => withGenericArgument; }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_Method_Exception() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_Method_Exception(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -559,22 +605,26 @@ public void MyVoidMethod() { } string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the MyVoidMethod summary. - /// The null reference exception thrown by MyVoidMethod. - /// These are the MyVoidMethod remarks. - public void MyVoidMethod() { } + /// + /// This is the MyVoidMethod summary. + /// + /// The null reference exception thrown by MyVoidMethod." + +GetRemarks(skipRemarks, "MyVoidMethod", " ") + +@" public void MyVoidMethod() { } }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_Field() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_Field(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -607,21 +657,25 @@ public class MyClass string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the MyField summary. - /// These are the MyField remarks. - public double MyField; + /// + /// This is the MyField summary. + /// " + +GetRemarks(skipRemarks, "MyField", " ") + +@" public double MyField; }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_PropertyWithSetter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_PropertyWithSetter(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -656,22 +710,26 @@ public class MyClass string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the MySetProperty summary. - /// This is the MySetProperty value. - /// These are the MySetProperty remarks. - public double MySetProperty { set; } + /// + /// This is the MySetProperty summary. + /// + /// This is the MySetProperty value." + +GetRemarks(skipRemarks, "MySetProperty", " ") + +@" public double MySetProperty { set; } }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_PropertyWithGetter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_PropertyWithGetter(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -706,22 +764,26 @@ public class MyClass string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the MyGetProperty summary. - /// This is the MyGetProperty value. - /// These are the MyGetProperty remarks. - public double MyGetProperty { get; } + /// + /// This is the MyGetProperty summary. + /// + /// This is the MyGetProperty value." + +GetRemarks(skipRemarks, "MyGetProperty", " ") + +@" public double MyGetProperty { get; } }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_PropertyWithGetterAndSetter() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_PropertyWithGetterAndSetter(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -756,22 +818,26 @@ public class MyClass string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the MyGetSetProperty summary. - /// This is the MyGetSetProperty value. - /// These are the MyGetSetProperty remarks. - public double MyGetSetProperty { get; set; } + /// + /// This is the MyGetSetProperty summary. + /// + /// This is the MyGetSetProperty value." + +GetRemarks(skipRemarks, "MyGetSetProperty", " ") + +@" public double MyGetSetProperty { get; set; } }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_Property_Exception() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_Property_Exception(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -807,23 +873,27 @@ public class MyClass string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the MyGetSetProperty summary. + /// + /// This is the MyGetSetProperty summary. + /// /// This is the MyGetSetProperty value. - /// The null reference exception thrown by MyGetSetProperty. - /// These are the MyGetSetProperty remarks. - public double MyGetSetProperty { get; set; } + /// The null reference exception thrown by MyGetSetProperty." + +GetRemarks(skipRemarks, "MyGetSetProperty", " ") + +@" public double MyGetSetProperty { get; set; } }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_Event() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_Event(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -856,21 +926,25 @@ public class MyClass string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the MyEvent summary. - /// These are the MyEvent remarks. - public event MyDelegate MyEvent; + /// + /// This is the MyEvent summary. + /// " + +GetRemarks(skipRemarks, "MyEvent", " ") + +@" public event MyDelegate MyEvent; }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_WithDelegate() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_WithDelegate(bool skipRemarks) { string topLevelTypeDocId = "T:MyNamespace.MyClass"; string delegateDocId = "T:MyNamespace.MyClass.MyDelegate"; @@ -910,22 +984,26 @@ public class MyClass string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the MyDelegate summary. - /// This is the MyDelegate sender description. - /// These are the MyDelegate remarks. - public delegate void MyDelegate(object sender); + /// + /// This is the MyDelegate summary. + /// + /// This is the MyDelegate sender description." + +GetRemarks(skipRemarks, "MyDelegate", " ") + +@" public delegate void MyDelegate(object sender); }"; List docFiles = new() { docFile1, docFile2 }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { topLevelTypeDocId, expectedCode }, { delegateDocId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task NestedEnum_InClass() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task NestedEnum_InClass(bool skipRemarks) { string topLevelTypeDocId = "T:MyNamespace.MyClass"; string enumDocId = "T:MyNamespace.MyClass.MyEnum"; @@ -979,17 +1057,25 @@ public enum MyEnum }"; string expectedCode = @"namespace MyNamespace; -/// This is the MyClass summary. -/// These are the MyClass remarks. -public class MyClass +/// +/// This is the MyClass summary. +/// " + +GetRemarks(skipRemarks, "MyClass") + +@"public class MyClass { - /// This is the MyEnum summary. - /// These are the MyEnum remarks. - public enum MyEnum + /// + /// This is the MyEnum summary. + /// " + +GetRemarks(skipRemarks, "MyEnum", " ") + +@" public enum MyEnum { - /// This is the MyEnum.Value1 summary. + /// + /// This is the MyEnum.Value1 summary. + /// Value1, - /// This is the MyEnum.Value2 summary. + /// + /// This is the MyEnum.Value2 summary. + /// Value2 } }"; @@ -997,13 +1083,15 @@ public enum MyEnum List docFiles = new() { docFile1, docFile2 }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { topLevelTypeDocId, expectedCode }, { enumDocId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task NestedStruct_InClass() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task NestedStruct_InClass(bool skipRemarks) { string topLevelTypeDocId = "T:MyNamespace.MyClass"; string enumDocId = "T:MyNamespace.MyClass.MyStruct"; @@ -1043,13 +1131,17 @@ public struct MyStruct }"; string expectedCode = @"namespace MyNamespace; -/// This is the MyClass summary. -/// These are the MyClass remarks. -public class MyClass +/// +/// This is the MyClass summary. +/// " + +GetRemarks(skipRemarks, "MyClass") + +@"public class MyClass { - /// This is the MyStruct summary. - /// These are the MyStruct remarks. - public struct MyStruct + /// + /// This is the MyStruct summary. + /// " + +GetRemarks(skipRemarks, "MyStruct", " ") + +@" public struct MyStruct { } }"; @@ -1057,13 +1149,15 @@ public struct MyStruct List docFiles = new() { docFile1, docFile2 }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { topLevelTypeDocId, expectedCode }, { enumDocId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_Operator() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_Operator(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -1099,24 +1193,28 @@ public class MyClass string expectedCode = @"namespace MyNamespace; public class MyClass { - /// This is the + operator summary. + /// + /// This is the + operator summary. + /// /// This is the + operator value1 description. /// This is the + operator value2 description. - /// This is the + operator returns description. - /// These are the + operator remarks. - public static MyClass operator +(MyClass value1, MyClass value2) => value1; + /// This is the + operator returns description." + +GetRemarks(skipRemarks, "+ operator", " ") + +@" public static MyClass operator +(MyClass value1, MyClass value2) => value1; }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Class_Do_Not_Backport_Inherited_Docs() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Class_Do_Not_Backport_Inherited_Docs(bool skipRemarks) { // In PortToDocs we find the base class and get the documentation if there's none in the child type. // In PortToTripleSlash, we should not do that. We only backport what's found in the child type. @@ -1194,17 +1292,23 @@ public interface MyInterface }"; string interfaceExpectedCode = @"namespace MyNamespace; -/// This is the MyInterface summary. -/// These are the MyInterface remarks. -public interface MyInterface +/// +/// This is the MyInterface summary. +/// " + +GetRemarks(skipRemarks, "MyInterface") + +@"public interface MyInterface { - /// This is the MyInterface.MyVoidMethod summary. - /// These are the MyInterface.MyVoidMethod remarks. - public void MyVoidMethod(); - /// This is the MyInterface.MyGetSetProperty summary. - /// This is the MyInterface.MyGetSetProperty value. - /// These are the MyInterface.MyGetSetProperty remarks. - public double MyGetSetProperty { get; set; } + /// + /// This is the MyInterface.MyVoidMethod summary. + /// " + +GetRemarks(skipRemarks, "MyInterface.MyVoidMethod", " ") + +@" public void MyVoidMethod(); + /// + /// This is the MyInterface.MyGetSetProperty summary. + /// + /// This is the MyInterface.MyGetSetProperty value." + +GetRemarks(skipRemarks, "MyInterface.MyGetSetProperty", " ") + +@" public double MyGetSetProperty { get; set; } }"; string classOriginalCode = @"namespace MyNamespace; @@ -1215,12 +1319,14 @@ public void MyVoidMethod() { } }"; string classExpectedCode = @"namespace MyNamespace; -/// This is the MyClass summary. -/// These are the MyClass remarks. -public class MyClass : MyInterface -{ - /// These are the MyClass.MyVoidMethod remarks. - public void MyVoidMethod() { } +/// +/// This is the MyClass summary. +/// " + +GetRemarks(skipRemarks, "MyClass") + +@"public class MyClass : MyInterface +{" + +GetRemarks(skipRemarks, "MyClass.MyVoidMethod", " ") + +@" public void MyVoidMethod() { } /// This is the MyClass.MyGetSetProperty value. public double MyGetSetProperty { get; set; } }"; @@ -1230,11 +1336,13 @@ public void MyVoidMethod() { } Dictionary expectedCodeFiles = new() { { interfaceDocId, interfaceExpectedCode }, { classDocId, classExpectedCode } }; StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(data); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Preserve_DoubleSlash_Comments() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Preserve_DoubleSlash_Comments(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -1258,37 +1366,46 @@ public Task Preserve_DoubleSlash_Comments() "; - string originalCode = @"namespace MyNamespace; -// Comment on top of type -public class MyClass + string originalCode = @"namespace MyNamespace { - // Comment on top of constructor - public MyClass() { } + // Comment on top of type + public class MyClass + { + // Comment on top of constructor + public MyClass() { } + } }"; - string expectedCode = @"namespace MyNamespace; -/// This is the MyClass type summary. -/// These are the MyClass type remarks. -// Comment on top of type -public class MyClass + string expectedCode = @"namespace MyNamespace { - /// This is the MyClass constructor summary. - /// These are the MyClass constructor remarks. - // Comment on top of constructor - public MyClass() { } + // Comment on top of type + /// + /// This is the MyClass type summary. + /// " + +GetRemarks(skipRemarks, "MyClass type", " ") + +@" public class MyClass + { + // Comment on top of constructor + /// + /// This is the MyClass constructor summary. + /// " + +GetRemarks(skipRemarks, "MyClass constructor", " ") + +@" public MyClass() { } + } }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [ActiveIssue("https://github.com/dotnet/api-docs-sync/issues/149")] - [Fact] - public Task Override_Existing_TripleSlash_Comments() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Override_Existing_TripleSlash_Comments(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -1313,37 +1430,49 @@ public Task Override_Existing_TripleSlash_Comments() "; string originalCode = @"namespace MyNamespace { - /// Old MyClass type summary. - /// Old MyClass type remarks. - public class MyClass - { - /// Old MyClass constructor summary. - /// Old MyClass constructor remarks. - public MyClass() { } - } + /// Replaceable MyClass type summary. + /// Unreplaceable MyClass type remarks. + public class MyClass + { + /// Unreplaceable MyClass constructor summary. + /// Replaceable MyClass constructor remarks. + public MyClass() { } + } }"; + string ctorRemarks = skipRemarks ? @" + /// Replaceable MyClass constructor remarks. +" : @" + /// New MyClass constructor remarks. +"; + + // The type remarks must always remain untouched: If skipRemarks is true, they're preexisting. If skipRemarks is false, there's no replacement. + // The member remarks must only change if skipRemarks is false, otherwise the old ones need to remain untouched. string expectedCode = @"namespace MyNamespace { - /// New MyClass type summary. - /// Old MyClass type remarks. - public class MyClass - { - /// Old MyClass constructor summary. - /// New MyClass constructor remarks. - public MyClass() { } - } + /// + /// New MyClass type summary. + /// + /// Unreplaceable MyClass type remarks. + public class MyClass + { + /// Unreplaceable MyClass constructor summary." + +ctorRemarks + +@" public MyClass() { } + } }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Full_Enum() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Full_Enum(bool skipRemarks) { string docId = "T:MyNamespace.MyEnum"; @@ -1380,13 +1509,19 @@ public enum MyEnum }"; string expectedCode = @"namespace MyNamespace; -/// This is the MyEnum summary. -/// These are the MyEnum remarks. -public enum MyEnum +/// +/// This is the MyEnum summary. +/// " + +GetRemarks(skipRemarks, "MyEnum") + +@"public enum MyEnum { - /// This is the MyEnum.Value1 summary. + /// + /// This is the MyEnum.Value1 summary. + /// Value1, - /// This is the MyEnum.Value2 summary. + /// + /// This is the MyEnum.Value2 summary. + /// Value2 }"; @@ -1394,13 +1529,15 @@ public enum MyEnum List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Full_Class() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Full_Class(bool skipRemarks) { string docId = "T:MyNamespace.MyClass"; @@ -1519,65 +1656,89 @@ public void MyVoidMethod() { } }"; string expectedCode = @"namespace MyNamespace; -/// This is the MyClass summary. -/// These are the MyClass remarks. -public class MyClass +/// +/// This is the MyClass summary. +/// " + +GetRemarks(skipRemarks, "MyClass") + +@"public class MyClass { - /// This is the MyClass constructor summary. - /// These are the MyClass constructor remarks. - public MyClass() { } - /// This is the MyClass constructor summary. - /// This is the MyClass constructor parameter description. - /// These are the MyClass constructor remarks. - public MyClass(int intParam) { } - /// This is the MyVoidMethod summary. - /// The null reference exception thrown by MyVoidMethod. - /// These are the MyVoidMethod remarks. - public void MyVoidMethod() { } - /// This is the MyIntMethod summary. + /// + /// This is the MyClass constructor summary. + /// " + +GetRemarks(skipRemarks, "MyClass constructor", " ") + +@" public MyClass() { } + /// + /// This is the MyClass constructor summary. + /// + /// This is the MyClass constructor parameter description." + +GetRemarks(skipRemarks, "MyClass constructor", " ") + +@" public MyClass(int intParam) { } + /// + /// This is the MyVoidMethod summary. + /// + /// The null reference exception thrown by MyVoidMethod." + +GetRemarks(skipRemarks, "MyVoidMethod", " ") + +@" public void MyVoidMethod() { } + /// + /// This is the MyIntMethod summary. + /// /// This is the MyIntMethod withArgument description. - /// This is the MyIntMethod returns description. - /// These are the MyIntMethod remarks. - public int MyIntMethod(int withArgument) => withArgument; - /// This is the MyGenericMethod summary. + /// This is the MyIntMethod returns description." + +GetRemarks(skipRemarks, "MyIntMethod", " ") + +@" public int MyIntMethod(int withArgument) => withArgument; + /// + /// This is the MyGenericMethod summary. + /// /// This is the MyGenericMethod type parameter description. /// This is the MyGenericMethod withGenericArgument description. - /// This is the MyGenericMethod returns description. - /// These are the MyGenericMethod remarks. - public T MyGenericMethod(T withGenericArgument) => withGenericArgument; - /// This is the MyField summary. - /// These are the MyField remarks. - public double MyField; - /// This is the MySetProperty summary. - /// This is the MySetProperty value. - /// These are the MySetProperty remarks. - public double MySetProperty { set => MyField = value; } - /// This is the MyGetProperty summary. - /// This is the MyGetProperty value. - /// These are the MyGetProperty remarks. - public double MyGetProperty => MyField; - /// This is the MyGetSetProperty summary. - /// This is the MyGetSetProperty value. - /// These are the MyGetSetProperty remarks. - public double MyGetSetProperty { get; set; } - /// This is the + operator summary. + /// This is the MyGenericMethod returns description." + +GetRemarks(skipRemarks, "MyGenericMethod", " ") + +@" public T MyGenericMethod(T withGenericArgument) => withGenericArgument; + /// + /// This is the MyField summary. + /// " + +GetRemarks(skipRemarks, "MyField", " ") + +@" public double MyField; + /// + /// This is the MySetProperty summary. + /// + /// This is the MySetProperty value." + +GetRemarks(skipRemarks, "MySetProperty", " ") + +@" public double MySetProperty { set => MyField = value; } + /// + /// This is the MyGetProperty summary. + /// + /// This is the MyGetProperty value." + +GetRemarks(skipRemarks, "MyGetProperty", " ") + +@" public double MyGetProperty => MyField; + /// + /// This is the MyGetSetProperty summary. + /// + /// This is the MyGetSetProperty value." + +GetRemarks(skipRemarks, "MyGetSetProperty", " ") + +@" public double MyGetSetProperty { get; set; } + /// + /// This is the + operator summary. + /// /// This is the + operator value1 description. /// This is the + operator value2 description. - /// This is the + operator returns description. - /// These are the + operator remarks. - public static MyClass operator +(MyClass value1, MyClass value2) => value1; + /// This is the + operator returns description." + +GetRemarks(skipRemarks, "+ operator", " ") + +@" public static MyClass operator +(MyClass value1, MyClass value2) => value1; }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Full_Struct() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Full_Struct(bool skipRemarks) { string docId = "T:MyNamespace.MyStruct"; @@ -1695,64 +1856,88 @@ public void MyVoidMethod() { } }"; string expectedCode = @"namespace MyNamespace; -/// This is the MyStruct summary. -/// These are the MyStruct remarks. -public struct MyStruct +/// +/// This is the MyStruct summary. +/// " + +GetRemarks(skipRemarks, "MyStruct") + +@"public struct MyStruct { - /// This is the MyStruct constructor summary. - /// These are the MyStruct constructor remarks. - public MyStruct() { } - /// This is the MyStruct constructor summary. - /// This is the MyStruct constructor parameter description. - /// These are the MyStruct constructor remarks. - public MyStruct(int intParam) { } - /// This is the MyVoidMethod summary. - /// These are the MyVoidMethod remarks. - public void MyVoidMethod() { } - /// This is the MyIntMethod summary. + /// + /// This is the MyStruct constructor summary. + /// " + +GetRemarks(skipRemarks, "MyStruct constructor", " ") + +@" public MyStruct() { } + /// + /// This is the MyStruct constructor summary. + /// + /// This is the MyStruct constructor parameter description." + +GetRemarks(skipRemarks, "MyStruct constructor", " ") + +@" public MyStruct(int intParam) { } + /// + /// This is the MyVoidMethod summary. + /// " + +GetRemarks(skipRemarks, "MyVoidMethod", " ") + +@" public void MyVoidMethod() { } + /// + /// This is the MyIntMethod summary. + /// /// This is the MyIntMethod withArgument description. - /// This is the MyIntMethod returns description. - /// These are the MyIntMethod remarks. - public int MyIntMethod(int withArgument) => withArgument; - /// This is the MyGenericMethod summary. + /// This is the MyIntMethod returns description." + +GetRemarks(skipRemarks, "MyIntMethod", " ") + +@" public int MyIntMethod(int withArgument) => withArgument; + /// + /// This is the MyGenericMethod summary. + /// /// This is the MyGenericMethod type parameter description. /// This is the MyGenericMethod withGenericArgument description. - /// This is the MyGenericMethod returns description. - /// These are the MyGenericMethod remarks. - public T MyGenericMethod(T withGenericArgument) => withGenericArgument; - /// This is the MyField summary. - /// These are the MyField remarks. - public double MyField; - /// This is the MySetProperty summary. - /// This is the MySetProperty value. - /// These are the MySetProperty remarks. - public double MySetProperty { set => MyField = value; } - /// This is the MyGetProperty summary. - /// This is the MyGetProperty value. - /// These are the MyGetProperty remarks. - public double MyGetProperty => MyField; - /// This is the MyGetSetProperty summary. - /// This is the MyGetSetProperty value. - /// These are the MyGetSetProperty remarks. - public double MyGetSetProperty { get; set; } - /// This is the + operator summary. + /// This is the MyGenericMethod returns description." + +GetRemarks(skipRemarks, "MyGenericMethod", " ") + +@" public T MyGenericMethod(T withGenericArgument) => withGenericArgument; + /// + /// This is the MyField summary. + /// " + +GetRemarks(skipRemarks, "MyField", " ") + +@" public double MyField; + /// + /// This is the MySetProperty summary. + /// + /// This is the MySetProperty value." + +GetRemarks(skipRemarks, "MySetProperty", " ") + +@" public double MySetProperty { set => MyField = value; } + /// + /// This is the MyGetProperty summary. + /// + /// This is the MyGetProperty value." + +GetRemarks(skipRemarks, "MyGetProperty", " ") + +@" public double MyGetProperty => MyField; + /// + /// This is the MyGetSetProperty summary. + /// + /// This is the MyGetSetProperty value." + +GetRemarks(skipRemarks, "MyGetSetProperty", " ") + +@" public double MyGetSetProperty { get; set; } + /// + /// This is the + operator summary. + /// /// This is the + operator value1 description. /// This is the + operator value2 description. - /// This is the + operator returns description. - /// These are the + operator remarks. - public static MyStruct operator +(MyStruct value1, MyStruct value2) => value1; + /// This is the + operator returns description." + +GetRemarks(skipRemarks, "+ operator", " ") + +@" public static MyStruct operator +(MyStruct value1, MyStruct value2) => value1; }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks); } - [Fact] - public Task Full_Interface() + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task Full_Interface(bool skipRemarks) { string docId = "T:MyNamespace.MyInterface"; @@ -1834,54 +2019,258 @@ public interface MyInterface }"; string expectedCode = @"namespace MyNamespace; -/// This is the MyInterface summary. -/// These are the MyInterface remarks. -public interface MyInterface +/// +/// This is the MyInterface summary. +/// " + +GetRemarks(skipRemarks, "MyInterface") + +@"public interface MyInterface { - /// This is the MyVoidMethod summary. - /// These are the MyVoidMethod remarks. - public void MyVoidMethod(); - /// This is the MyIntMethod summary. + /// + /// This is the MyVoidMethod summary. + /// " + +GetRemarks(skipRemarks, "MyVoidMethod", " ") + +@" public void MyVoidMethod(); + /// + /// This is the MyIntMethod summary. + /// /// This is the MyIntMethod withArgument description. - /// This is the MyIntMethod returns description. - /// These are the MyIntMethod remarks. - public int MyIntMethod(int withArgument); - /// This is the MyGenericMethod summary. + /// This is the MyIntMethod returns description." + +GetRemarks(skipRemarks, "MyIntMethod", " ") + +@" public int MyIntMethod(int withArgument); + /// + /// This is the MyGenericMethod summary. + /// /// This is the MyGenericMethod type parameter description. /// This is the MyGenericMethod withGenericArgument description. - /// This is the MyGenericMethod returns description. - /// These are the MyGenericMethod remarks. - public T MyGenericMethod(T withGenericArgument); - /// This is the MySetProperty summary. - /// This is the MySetProperty value. - /// These are the MySetProperty remarks. - public double MySetProperty { set; } - /// This is the MyGetProperty summary. - /// This is the MyGetProperty value. - /// These are the MyGetProperty remarks. - public double MyGetProperty { get; } - /// This is the MyGetSetProperty summary. - /// This is the MyGetSetProperty value. - /// These are the MyGetSetProperty remarks. - public double MyGetSetProperty { get; set; } + /// This is the MyGenericMethod returns description." + +GetRemarks(skipRemarks, "MyGenericMethod", " ") + +@" public T MyGenericMethod(T withGenericArgument); + /// + /// This is the MySetProperty summary. + /// + /// This is the MySetProperty value." + +GetRemarks(skipRemarks, "MySetProperty", " ") + +@" public double MySetProperty { set; } + /// + /// This is the MyGetProperty summary. + /// + /// This is the MyGetProperty value." + +GetRemarks(skipRemarks, "MyGetProperty", " ") + +@" public double MyGetProperty { get; } + /// + /// This is the MyGetSetProperty summary. + /// + /// This is the MyGetSetProperty value." + +GetRemarks(skipRemarks, "MyGetSetProperty", " ") + +@" public double MyGetSetProperty { get; set; } +}"; + + List docFiles = new() { docFile }; + List originalCodeFiles = new() { originalCode }; + Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + + return TestWithStringsAsync(data, skipRemarks); + } + + [Fact] + public Task Class_Convert_Generics_Percent601_MarkdownRemarks() + { + string docMyGenericType = @" + + + MyAssembly + + + This is the MyGenericType{T} class summary. + + . + ]]> + + + +"; + + string docMyGenericTypeEnumerator = @" + + + MyAssembly + + + This is the MyGenericType{T}.Enumerator class summary. + + +"; + + string originalCode = @"using System; + +namespace MyNamespace +{ + public class MyGenericType + { + public class Enumerator { } + } +}"; + + string expectedCode = @"using System; + +namespace MyNamespace +{ + /// + /// This is the MyGenericType{T} class summary. + /// + /// Contains the nested class . + public class MyGenericType + { + /// + /// This is the MyGenericType{T}.Enumerator class summary. + /// + public class Enumerator { } + } +}"; + + List docFiles = new() { docMyGenericType, docMyGenericTypeEnumerator }; + List originalCodeFiles = new() { originalCode }; + Dictionary expectedCodeFiles = new() + { + { "T:MyNamespace.MyGenericType`1", expectedCode }, + { "T:MyNamespace.MyGenericType`1.Enumerator", expectedCode } + }; + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + + return TestWithStringsAsync(data, skipRemarks: false); + } + + [Fact] + public Task Class_Preserve_URLEntities_MarkdownRemarks() + { + string docId = "T:MyNamespace.MyClass"; + + string docFile = @" + + + MyAssembly + + + To be added. + + + + + + +"; + + string originalCode = @"using System; + +namespace MyNamespace +{ + public class MyClass + { + } +}"; + + string expectedCode = @"using System; + +namespace MyNamespace +{ + /// URL entities: %23%28%2C%29 must remain unconverted. + public class MyClass + { + } +}"; + + List docFiles = new() { docFile }; + List originalCodeFiles = new() { originalCode }; + Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + + return TestWithStringsAsync(data, skipRemarks: false); + } + + [Fact] + public Task Class_Multiline_MarkdownRemarks() + { + string docId = "T:MyNamespace.MyClass"; + + string docFile = @" + + + MyAssembly + + + To be added. + + + + + + +"; + + string originalCode = @"using System; + +namespace MyNamespace +{ + public class MyClass + { + } +}"; + + string expectedCode = @"using System; + +namespace MyNamespace +{ + /// Line 1. + /// Line 2. + /// Line 3. + public class MyClass + { + } }"; List docFiles = new() { docFile }; List originalCodeFiles = new() { originalCode }; Dictionary expectedCodeFiles = new() { { docId, expectedCode } }; - StringTestData stringTestData = new(docFiles, originalCodeFiles, expectedCodeFiles, false); + StringTestData data = new(docFiles, originalCodeFiles, expectedCodeFiles, false); - return TestWithStringsAsync(stringTestData); + return TestWithStringsAsync(data, skipRemarks: false); + } + + private static string GetRemarks(bool skipRemarks, string apiName, string spacing = "") + { + return skipRemarks ? @" +" : $@" +{spacing}/// These are the {apiName} remarks. +"; } - private static Task TestWithStringsAsync(StringTestData stringTestData) => - TestWithStringsAsync(new Configuration() { SkipInterfaceImplementations = false }, DefaultAssembly, stringTestData); + private static Task TestWithStringsAsync(StringTestData data, bool skipRemarks) => + TestWithStringsAsync(new Configuration() { SkipInterfaceImplementations = false, SkipRemarks = skipRemarks }, DefaultAssembly, data); private static async Task TestWithStringsAsync(Configuration c, string assembly, StringTestData data) { - Assert.True(data.XDocs.Any(), "No XDoc elements passed."); - Assert.True(data.OriginalCodeFiles.Any(), "No original code files passed."); - Assert.True(data.ExpectedCodeFiles.Any(), "No expected code files passed."); + Assert.NotEmpty(data.XDocs); + Assert.NotEmpty(data.OriginalCodeFiles); + Assert.NotEmpty(data.ExpectedCodeFiles); c.IncludedAssemblies.Add(assembly); @@ -1932,8 +2321,8 @@ private static async Task TestWithStringsAsync(Configuration c, string assembly, Assert.True(symbolLocations.Any(), $"No symbol locations found for {resultDocId}."); foreach (ResolvedLocation location in symbolLocations) { - string newNode = location.NewNode.ToFullString(); - Assert.Equal(expectedCode, newNode); + string actualCode = location.NewNode.ToFullString(); + Assert.Equal(expectedCode, actualCode); } } } diff --git a/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Basic/MyType.xml b/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Basic/MyType.xml index 35a1dde..f1beaec 100644 --- a/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Basic/MyType.xml +++ b/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Basic/MyType.xml @@ -1,4 +1,4 @@ - + MyAssembly @@ -13,7 +13,7 @@ These are the class remarks. -URL entities: %23%28%29%2C. +These URL entities should be converted: %23%28%29%2C. Multiple lines. diff --git a/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index 134444d..0a3468c 100644 --- a/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -1,25 +1,32 @@ -using System; +using System; namespace MyNamespace { - /// This is the MyEnum enum summary. - /// enum remarks. They contain an [!INCLUDE[MyInclude](~/includes/MyInclude.md)] which should prevent converting markdown to xml. - /// URL entities: %23%28%2C%29 must remain unconverted. - /// ]]> // Original MyEnum enum comments with information for maintainers, must stay. + /// + /// This is the MyEnum enum summary. + /// + /// These are the enum remarks. They contain an [!INCLUDE[MyInclude](~/includes/MyInclude.md)] which should prevent converting markdown to xml. + /// URL entities: %23%28%2C%29 must remain unconverted. public enum MyEnum { - /// This is the MyEnumValue0 member summary. There is no public modifier. + /// + /// This is the MyEnumValue0 member summary. There is no public modifier. + /// MyEnumValue0 = 0, - /// This is the MyEnumValue1 member summary. There is no public modifier. + /// + /// This is the MyEnumValue1 member summary. There is no public modifier. + /// MyEnumValue1 = 1 } - /// This is the MyType class summary. + // Original MyType class comments with information for maintainers, must stay. + /// + /// This is the MyType class summary. + /// /// These are the class remarks. - /// URL entities: #(),. + /// These URL entities should be converted: #(),. /// Multiple lines. /// [!NOTE] @@ -27,12 +34,13 @@ public enum MyEnum /// ]]> /// This text is not a note. It has a that should be xml and outside the cdata. /// Long xrefs one after the other: or should both be converted to crefs. - // Original MyType class comments with information for maintainers, must stay. public class MyType { - /// This is the MyType constructor summary. // Original MyType constructor double slash comments on top of triple slash, with information for maintainers, must stay but after triple slash. // Original MyType constructor double slash comments on bottom of triple slash, with information for maintainers, must stay. + /// + /// This is the MyType constructor summary. + /// public MyType() { } /* Trailing comments should remain untouched */ @@ -51,20 +59,24 @@ internal MyType(int myProperty) // Double slash comments above private members should remain untouched. private int _myProperty; - /// This is the MyProperty summary. + // Original MyProperty property double slash comments with information for maintainers, must stay. + // This particular example has two rows of double slash comments and both should stay. + /// + /// This is the MyProperty summary. + /// /// This is the MyProperty value. /// These are the MyProperty remarks. /// Multiple lines and a reference to the field and the xref uses displayProperty, which should be ignored when porting. - // Original MyProperty property double slash comments with information for maintainers, must stay. - // This particular example has two rows of double slash comments and both should stay. public int MyProperty { get { return _myProperty; /* Internal comments should remain untouched. */ } set { _myProperty = value; } // Internal comments should remain untouched } - /// This is the MyField summary. - /// There is a primitive type here. + /// + /// This is the MyField summary. + /// There is a primitive type here. + /// /// These are the MyField remarks. /// There is a primitive type here. /// Multiple lines. @@ -80,7 +92,9 @@ public int MyProperty /// public int MyField = 1; - /// This is the MyIntMethod summary. + /// + /// This is the MyIntMethod summary. + /// /// This is the MyIntMethod param1 summary. /// This is the MyIntMethod param2 summary. /// This is the MyIntMethod return value. It mentions the . @@ -98,7 +112,9 @@ public int MyIntMethod(int param1, int param2) return MyField + param1 + param2; } - /// This is the MyVoidMethod summary. + /// + /// This is the MyVoidMethod summary. + /// /// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . /// This is the IndexOutOfRangeException thrown by MyVoidMethod. /// -or- @@ -128,7 +144,9 @@ public void UndocumentedMethod() if (MyEvent == null) { } // Use MyEvent to remove the unused warning } - /// This is the MyTypeParamMethod summary. + /// + /// This is the MyTypeParamMethod summary. + /// /// This is the MyTypeParamMethod typeparam T. /// This is the MyTypeParamMethod parameter param1. /// This is a reference to the typeparam . @@ -139,7 +157,10 @@ public void MyTypeParamMethod(int param1) { } - /// This is the MyDelegate summary. + // Original MyDelegate delegate comments with information for maintainers, must stay. + /// + /// This is the MyDelegate summary. + /// /// This is the sender parameter. /// These are the remarks. There is a code example, which should be moved to its own examples section: /// Here is some text in the examples section. There is an that should be converted to xml. @@ -153,18 +174,19 @@ public void MyTypeParamMethod(int param1) /// /// /// The .NET Runtime repo. - // Original MyDelegate delegate comments with information for maintainers, must stay. public delegate void MyDelegate(object sender); /// This is the MyEvent summary. public event MyDelegate MyEvent; - /// Adds two MyType instances. + // Original operator + method comments with information for maintainers, must stay. + /// + /// Adds two MyType instances. + /// /// The first type to add. /// The second type to add. /// The added types. /// These are the remarks. They are in plain xml and should be transferred unmodified. - // Original operator + method comments with information for maintainers, must stay. public static MyType operator +(MyType value1, MyType value2) => value1; } } diff --git a/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Generics/MyAssembly.csproj b/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Generics/MyAssembly.csproj deleted file mode 100644 index c51659e..0000000 --- a/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Generics/MyAssembly.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Library - This is MyNamespace description. - net7.0 - false - - - - - - - - diff --git a/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Generics/MyGenericType`1+Enumerator.xml b/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Generics/MyGenericType`1+Enumerator.xml deleted file mode 100644 index 29f77af..0000000 --- a/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Generics/MyGenericType`1+Enumerator.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - MyAssembly - - - This is the MyGenericType{T}.Enumerator class summary. - - diff --git a/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Generics/MyGenericType`1.xml b/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Generics/MyGenericType`1.xml deleted file mode 100644 index f3881f0..0000000 --- a/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Generics/MyGenericType`1.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - MyAssembly - - - This is the MyGenericType{T} class summary. - - - . - ]]> - - - diff --git a/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Generics/SourceExpected.cs b/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Generics/SourceExpected.cs deleted file mode 100644 index ae85c0f..0000000 --- a/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Generics/SourceExpected.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace MyNamespace -{ - /// This is the MyGenericType{T} class summary. - /// Contains the nested class . - // Original MyGenericType class comments with information for maintainers, must stay. - public class MyGenericType - { - /// This is the MyGenericType{T}.Enumerator class summary. - // Original MyGenericType.Enumerator class comments with information for maintainers, must stay. - public class Enumerator { } - } -} diff --git a/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Generics/SourceOriginal.cs b/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Generics/SourceOriginal.cs deleted file mode 100644 index 3d91be3..0000000 --- a/src/PortToTripleSlash/tests/PortToTripleSlash/TestData/Generics/SourceOriginal.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace MyNamespace -{ - // Original MyGenericType class comments with information for maintainers, must stay. - public class MyGenericType - { - // Original MyGenericType.Enumerator class comments with information for maintainers, must stay. - public class Enumerator { } - } -} diff --git a/src/PortToTripleSlash/tests/tests.csproj b/src/PortToTripleSlash/tests/tests.csproj index 62ae747..29c5e3f 100644 --- a/src/PortToTripleSlash/tests/tests.csproj +++ b/src/PortToTripleSlash/tests/tests.csproj @@ -13,17 +13,11 @@ - - - - - -