From a0aa93f1419d86097561743f40cb3de13a9d6b15 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:41:29 -0800 Subject: [PATCH 01/65] Move locations --- DocsPortingTool/Analyzer.cs | 898 ------------------ DocsPortingTool/Configuration.cs | 575 ----------- DocsPortingTool/Docs/APIKind.cs | 12 - DocsPortingTool/Docs/DocsAPI.cs | 234 ----- DocsPortingTool/Docs/DocsAssemblyInfo.cs | 38 - DocsPortingTool/Docs/DocsAttribute.cs | 29 - DocsPortingTool/Docs/DocsCommentsContainer.cs | 303 ------ DocsPortingTool/Docs/DocsException.cs | 103 -- DocsPortingTool/Docs/DocsMember.cs | 224 ----- DocsPortingTool/Docs/DocsMemberSignature.cs | 30 - DocsPortingTool/Docs/DocsParam.cs | 41 - DocsPortingTool/Docs/DocsParameter.cs | 27 - DocsPortingTool/Docs/DocsType.cs | 184 ---- DocsPortingTool/Docs/DocsTypeParam.cs | 42 - DocsPortingTool/Docs/DocsTypeParameter.cs | 64 -- DocsPortingTool/Docs/DocsTypeSignature.cs | 30 - DocsPortingTool/Docs/IDocsAPI.cs | 22 - DocsPortingTool/DocsPortingTool.cs | 12 - DocsPortingTool/DocsPortingTool.csproj | 18 - DocsPortingTool/Extensions.cs | 37 - DocsPortingTool/Log.cs | 377 -------- .../Properties/launchSettings.json | 15 - .../TripleSlashCommentsContainer.cs | 186 ---- .../TripleSlash/TripleSlashException.cs | 49 - .../TripleSlash/TripleSlashMember.cs | 160 ---- .../TripleSlash/TripleSlashParam.cs | 44 - .../TripleSlash/TripleSlashTypeParam.cs | 40 - DocsPortingTool/XmlHelper.cs | 319 ------- 28 files changed, 4113 deletions(-) delete mode 100644 DocsPortingTool/Analyzer.cs delete mode 100644 DocsPortingTool/Configuration.cs delete mode 100644 DocsPortingTool/Docs/APIKind.cs delete mode 100644 DocsPortingTool/Docs/DocsAPI.cs delete mode 100644 DocsPortingTool/Docs/DocsAssemblyInfo.cs delete mode 100644 DocsPortingTool/Docs/DocsAttribute.cs delete mode 100644 DocsPortingTool/Docs/DocsCommentsContainer.cs delete mode 100644 DocsPortingTool/Docs/DocsException.cs delete mode 100644 DocsPortingTool/Docs/DocsMember.cs delete mode 100644 DocsPortingTool/Docs/DocsMemberSignature.cs delete mode 100644 DocsPortingTool/Docs/DocsParam.cs delete mode 100644 DocsPortingTool/Docs/DocsParameter.cs delete mode 100644 DocsPortingTool/Docs/DocsType.cs delete mode 100644 DocsPortingTool/Docs/DocsTypeParam.cs delete mode 100644 DocsPortingTool/Docs/DocsTypeParameter.cs delete mode 100644 DocsPortingTool/Docs/DocsTypeSignature.cs delete mode 100644 DocsPortingTool/Docs/IDocsAPI.cs delete mode 100644 DocsPortingTool/DocsPortingTool.cs delete mode 100644 DocsPortingTool/DocsPortingTool.csproj delete mode 100644 DocsPortingTool/Extensions.cs delete mode 100644 DocsPortingTool/Log.cs delete mode 100644 DocsPortingTool/Properties/launchSettings.json delete mode 100644 DocsPortingTool/TripleSlash/TripleSlashCommentsContainer.cs delete mode 100644 DocsPortingTool/TripleSlash/TripleSlashException.cs delete mode 100644 DocsPortingTool/TripleSlash/TripleSlashMember.cs delete mode 100644 DocsPortingTool/TripleSlash/TripleSlashParam.cs delete mode 100644 DocsPortingTool/TripleSlash/TripleSlashTypeParam.cs delete mode 100644 DocsPortingTool/XmlHelper.cs diff --git a/DocsPortingTool/Analyzer.cs b/DocsPortingTool/Analyzer.cs deleted file mode 100644 index 540449a..0000000 --- a/DocsPortingTool/Analyzer.cs +++ /dev/null @@ -1,898 +0,0 @@ -#nullable enable -using DocsPortingTool.Docs; -using DocsPortingTool.TripleSlash; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace DocsPortingTool -{ - public class Analyzer - { - private readonly List ModifiedFiles = new List(); - private readonly List ModifiedTypes = new List(); - private readonly List ModifiedAPIs = new List(); - private readonly List ProblematicAPIs = new List(); - private readonly List AddedExceptions = new List(); - - private int TotalModifiedIndividualElements = 0; - - private readonly TripleSlashCommentsContainer TripleSlashComments; - private readonly DocsCommentsContainer DocsComments; - - private Configuration Config { get; set; } - - public Analyzer(Configuration config) - { - Config = config; - TripleSlashComments = new TripleSlashCommentsContainer(config); - DocsComments = new DocsCommentsContainer(config); - } - - // Do all the magic. - public void Start() - { - TripleSlashComments.CollectFiles(); - - if (TripleSlashComments.TotalFiles > 0) - { - DocsComments.CollectFiles(); - PortMissingComments(); - } - else - { - Log.Error("No triple slash comments found."); - } - - PrintUndocumentedAPIs(); - PrintSummary(); - - DocsComments.Save(); - } - - // Checks if the passed string is considered "empty" according to the Docs repo rules. - internal static bool IsEmpty(string? s) - { - return string.IsNullOrWhiteSpace(s) || s == Configuration.ToBeAdded; - } - - private void PortMissingComments() - { - Log.Info("Looking for triple slash comments that can be ported..."); - - foreach (DocsType dTypeToUpdate in DocsComments.Types) - { - PortMissingCommentsForType(dTypeToUpdate); - } - - foreach (DocsMember dMemberToUpdate in DocsComments.Members) - { - PortMissingCommentsForMember(dMemberToUpdate); - } - } - - // Tries to find a triple slash element from which to port documentation for the specified Docs type. - private void PortMissingCommentsForType(DocsType dTypeToUpdate) - { - TripleSlashMember? tsTypeToPort = TripleSlashComments.Members.FirstOrDefault(x => x.Name == dTypeToUpdate.DocIdEscaped); - if (tsTypeToPort != null) - { - if (tsTypeToPort.Name == dTypeToUpdate.DocIdEscaped) - { - TryPortMissingSummaryForAPI(dTypeToUpdate, tsTypeToPort, null); - TryPortMissingRemarksForAPI(dTypeToUpdate, tsTypeToPort, null, skipInterfaceRemarks: true); - TryPortMissingParamsForAPI(dTypeToUpdate, tsTypeToPort, null); // Some types, like delegates, have params - TryPortMissingTypeParamsForAPI(dTypeToUpdate, tsTypeToPort, null); // Type names ending with have TypeParams - } - - if (dTypeToUpdate.Changed) - { - ModifiedTypes.AddIfNotExists(dTypeToUpdate.DocId); - ModifiedFiles.AddIfNotExists(dTypeToUpdate.FilePath); - } - } - } - - // Tries to find a triple slash element from which to port documentation for the specified Docs member. - private void PortMissingCommentsForMember(DocsMember dMemberToUpdate) - { - string docId = dMemberToUpdate.DocIdEscaped; - TripleSlashMember? tsMemberToPort = TripleSlashComments.Members.FirstOrDefault(x => x.Name == docId); - TryGetEIIMember(dMemberToUpdate, out DocsMember? interfacedMember); - - if (tsMemberToPort != null || interfacedMember != null) - { - TryPortMissingSummaryForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember); - TryPortMissingRemarksForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember, Config.SkipInterfaceRemarks); - TryPortMissingParamsForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember); - TryPortMissingTypeParamsForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember); - TryPortMissingExceptionsForMember(dMemberToUpdate, tsMemberToPort); - - // Properties sometimes don't have a but have a - if (dMemberToUpdate.MemberType == "Property") - { - TryPortMissingPropertyForMember(dMemberToUpdate, tsMemberToPort, interfacedMember); - } - else if (dMemberToUpdate.MemberType == "Method") - { - TryPortMissingMethodForMember(dMemberToUpdate, tsMemberToPort, interfacedMember); - } - - if (dMemberToUpdate.Changed) - { - ModifiedAPIs.AddIfNotExists(dMemberToUpdate.DocId); - ModifiedFiles.AddIfNotExists(dMemberToUpdate.FilePath); - } - } - } - - // Gets a string indicating if an API is an explicit interface implementation, or empty. - private string GetIsEII(bool isEII) - { - return isEII ? " (EII) " : string.Empty; - } - - // Gets a string indicating if an API was created, otherwise it was modified. - private string GetIsCreated(bool created) - { - return created ? "Created" : "Modified"; - } - - // Attempts to obtain the member of the implemented interface. - private bool TryGetEIIMember(IDocsAPI dApiToUpdate, out DocsMember? interfacedMember) - { - interfacedMember = null; - - if (!Config.SkipInterfaceImplementations && dApiToUpdate is DocsMember member) - { - string interfacedMemberDocId = member.ImplementsInterfaceMember; - if (!string.IsNullOrEmpty(interfacedMemberDocId)) - { - interfacedMember = DocsComments.Members.FirstOrDefault(x => x.DocId == interfacedMemberDocId); - return interfacedMember != null; - } - } - - return false; - } - - // Ports the summary for the specified API if the field is undocumented. - private void TryPortMissingSummaryForAPI(IDocsAPI dApiToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember) - { - if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeSummaries || - dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberSummaries) - { - return; - } - - // Only port if undocumented in MS Docs - if (IsEmpty(dApiToUpdate.Summary)) - { - bool isEII = false; - - string name = string.Empty; - string value = string.Empty; - - // Try to port triple slash comments - if (tsMemberToPort != null && !IsEmpty(tsMemberToPort.Summary)) - { - dApiToUpdate.Summary = tsMemberToPort.Summary; - name = tsMemberToPort.Name; - value = tsMemberToPort.Summary; - } - // or try to find if it implements a documented interface - else if (interfacedMember != null && !IsEmpty(interfacedMember.Summary)) - { - dApiToUpdate.Summary = interfacedMember.Summary; - isEII = true; - name = interfacedMember.MemberName; - value = interfacedMember.Summary; - } - - if (!IsEmpty(value)) - { - // Any member can have an empty summary - string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} summary: {name.Escaped()} = {value.Escaped()}"; - PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); - TotalModifiedIndividualElements++; - } - } - } - - // Ports the remarks for the specified API if the field is undocumented. - private void TryPortMissingRemarksForAPI(IDocsAPI dApiToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember, bool skipInterfaceRemarks) - { - if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeRemarks || - dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberRemarks) - { - return; - } - - if (IsEmpty(dApiToUpdate.Remarks)) - { - bool isEII = false; - string name = string.Empty; - string value = string.Empty; - - // Try to port triple slash comments - if (tsMemberToPort != null && !IsEmpty(tsMemberToPort.Remarks)) - { - dApiToUpdate.Remarks = tsMemberToPort.Remarks; - name = tsMemberToPort.Name; - value = tsMemberToPort.Remarks; - } - // or try to find if it implements a documented interface - // which only happens in docs members (types have a null interfacedMember passed) - else if (interfacedMember != null && !IsEmpty(interfacedMember.Remarks)) - { - DocsMember memberToUpdate = (DocsMember)dApiToUpdate; - - // Only attempt to port if the member name is the same as the interfaced member docid without prefix - if (memberToUpdate.MemberName == interfacedMember.DocId[2..]) - { - string dMemberToUpdateTypeDocIdNoPrefix = memberToUpdate.ParentType.DocId[2..]; - string interfacedMemberTypeDocIdNoPrefix = interfacedMember.ParentType.DocId[2..]; - - // Special text for EIIs in Remarks - string eiiMessage = $"This member is an explicit interface member implementation. It can be used only when the instance is cast to an interface.{Environment.NewLine + Environment.NewLine}"; - - string cleanedInterfaceRemarks = string.Empty; - if (!interfacedMember.Remarks.Contains(Configuration.ToBeAdded)) - { - cleanedInterfaceRemarks = interfacedMember.Remarks.RemoveSubstrings("##Remarks", "## Remarks", ""); - } - - // Only port the interface remarks if the user desired that - if (!skipInterfaceRemarks) - { - dApiToUpdate.Remarks = eiiMessage + cleanedInterfaceRemarks; - } - // Otherwise, always add the EII special message - else - { - dApiToUpdate.Remarks = eiiMessage; - } - - name = interfacedMember.MemberName; - value = dApiToUpdate.Remarks; - - isEII = true; - } - } - - if (!IsEmpty(value)) - { - // Any member can have an empty remark - string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} remarks: {name.Escaped()} = {value.Escaped()}"; - PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); - TotalModifiedIndividualElements++; - } - } - } - - // Ports all the parameter descriptions for the specified API if any of them is undocumented. - private void TryPortMissingParamsForAPI(IDocsAPI dApiToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember) - { - if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeParams || - dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberParams) - { - return; - } - - bool created; - bool isEII; - string name; - string value; - - if (tsMemberToPort != null) - { - foreach (DocsParam dParam in dApiToUpdate.Params) - { - if (IsEmpty(dParam.Value)) - { - created = false; - isEII = false; - name = string.Empty; - value = string.Empty; - - TripleSlashParam? tsParam = tsMemberToPort.Params.FirstOrDefault(x => x.Name == dParam.Name); - - // When not found, it's a bug in Docs (param name not the same as source/ref), so need to ask the user to indicate correct name - if (tsParam == null) - { - ProblematicAPIs.AddIfNotExists($"Param=[{dParam.Name}] in Member DocId=[{dApiToUpdate.DocId}]"); - - if (tsMemberToPort.Params.Count() == 0) - { - ProblematicAPIs.AddIfNotExists($"Param=[{dParam.Name}] in Member DocId=[{dApiToUpdate.DocId}]"); - Log.Warning($" There were no triple slash comments for param '{dParam.Name}' in {dApiToUpdate.DocId}"); - } - else - { - created = TryPromptParam(dParam, tsMemberToPort, out TripleSlashParam? newTsParam); - if (newTsParam == null) - { - Log.Error($" There param '{dParam.Name}' was not found in triple slash for {dApiToUpdate.DocId}"); - } - else - { - // Now attempt to document it - if (!IsEmpty(newTsParam.Value)) - { - // try to port triple slash comments - dParam.Value = newTsParam.Value; - name = newTsParam.Name; - value = newTsParam.Value; - } - // or try to find if it implements a documented interface - else if (interfacedMember != null) - { - DocsParam? interfacedParam = interfacedMember.Params.FirstOrDefault(x => x.Name == newTsParam.Name || x.Name == dParam.Name); - if (interfacedParam != null) - { - dParam.Value = interfacedParam.Value; - name = interfacedParam.Name; - value = interfacedParam.Value; - isEII = true; - } - } - } - } - } - // Attempt to port - else if (!IsEmpty(tsParam.Value)) - { - // try to port triple slash comments - dParam.Value = tsParam.Value; - name = tsParam.Name; - value = tsParam.Value; - } - // or try to find if it implements a documented interface - else if (interfacedMember != null) - { - DocsParam? interfacedParam = interfacedMember.Params.FirstOrDefault(x => x.Name == dParam.Name); - if (interfacedParam != null) - { - dParam.Value = interfacedParam.Value; - name = interfacedParam.Name; - value = interfacedParam.Value; - isEII = true; - } - } - - - if (!IsEmpty(value)) - { - string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} ({GetIsCreated(created)}) param {name.Escaped()} = {value.Escaped()}"; - PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); - TotalModifiedIndividualElements++; - } - } - } - } - else if (interfacedMember != null) - { - foreach (DocsParam dParam in dApiToUpdate.Params) - { - if (IsEmpty(dParam.Value)) - { - DocsParam? interfacedParam = interfacedMember.Params.FirstOrDefault(x => x.Name == dParam.Name); - if (interfacedParam != null && !IsEmpty(interfacedParam.Value)) - { - dParam.Value = interfacedParam.Value; - - string message = $"{dApiToUpdate.Kind} EII ({GetIsCreated(false)}) param {dParam.Name.Escaped()} = {dParam.Value.Escaped()}"; - PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); - TotalModifiedIndividualElements++; - } - } - } - } - } - - // Ports all the type parameter descriptions for the specified API if any of them is undocumented. - private void TryPortMissingTypeParamsForAPI(IDocsAPI dApiToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember) - { - if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeTypeParams || - dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberTypeParams) - { - return; - } - - if (tsMemberToPort != null) - { - foreach (TripleSlashTypeParam tsTypeParam in tsMemberToPort.TypeParams) - { - bool isEII = false; - string name = string.Empty; - string value = string.Empty; - - DocsTypeParam? dTypeParam = dApiToUpdate.TypeParams.FirstOrDefault(x => x.Name == tsTypeParam.Name); - - bool created = false; - if (dTypeParam == null) - { - ProblematicAPIs.AddIfNotExists($"TypeParam=[{tsTypeParam.Name}] in Member=[{dApiToUpdate.DocId}]"); - dTypeParam = dApiToUpdate.AddTypeParam(tsTypeParam.Name, XmlHelper.GetNodesInPlainText(tsTypeParam.XETypeParam)); - created = true; - } - - // But it can still be empty, try to retrieve it - if (IsEmpty(dTypeParam.Value)) - { - // try to port triple slash comments - if (!IsEmpty(tsTypeParam.Value)) - { - name = tsTypeParam.Name; - value = tsTypeParam.Value; - } - // or try to find if it implements a documented interface - else if (interfacedMember != null) - { - DocsTypeParam? interfacedTypeParam = interfacedMember.TypeParams.FirstOrDefault(x => x.Name == dTypeParam.Name); - if (interfacedTypeParam != null) - { - name = interfacedTypeParam.Name; - value = interfacedTypeParam.Value; - isEII = true; - } - } - } - - if (!IsEmpty(value)) - { - dTypeParam.Value = value; - string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} ({GetIsCreated(created)}) typeparam {name.Escaped()} = {value.Escaped()}"; - PrintModifiedMember(message, dTypeParam.ParentAPI.FilePath, dApiToUpdate.DocId); - TotalModifiedIndividualElements++; - } - } - } - } - - // Tries to document the passed property. - private void TryPortMissingPropertyForMember(DocsMember dMemberToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember) - { - if (!Config.PortMemberProperties) - { - return; - } - - if (IsEmpty(dMemberToUpdate.Value)) - { - string name = string.Empty; - string value = string.Empty; - bool isEII = false; - - // Issue: sometimes properties have their TS string in Value, sometimes in Returns - if (tsMemberToPort != null) - { - name = tsMemberToPort.Name; - if (!IsEmpty(tsMemberToPort.Value)) - { - value = tsMemberToPort.Value; - } - else if (!IsEmpty(tsMemberToPort.Returns)) - { - value = tsMemberToPort.Returns; - } - } - // or try to find if it implements a documented interface - else if (interfacedMember != null) - { - name = interfacedMember.MemberName; - if (!IsEmpty(interfacedMember.Value)) - { - value = interfacedMember.Value; - } - else if (!IsEmpty(interfacedMember.Returns)) - { - value = interfacedMember.Returns; - } - if (!string.IsNullOrEmpty(value)) - { - isEII = true; - } - } - - if (!IsEmpty(value)) - { - dMemberToUpdate.Value = value; - string message = $"Member {GetIsEII(isEII)} property {name.Escaped()} = {value.Escaped()}"; - PrintModifiedMember(message, dMemberToUpdate.FilePath,dMemberToUpdate.DocId); - TotalModifiedIndividualElements++; - } - } - } - - // Tries to document the passed method. - private void TryPortMissingMethodForMember(DocsMember dMemberToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember) - { - if (!Config.PortMemberReturns) - { - return; - } - - if (IsEmpty(dMemberToUpdate.Returns)) - { - string name = string.Empty; - string value = string.Empty; - bool isEII = false; - - // Bug: Sometimes a void return value shows up as not documented, skip those - if (dMemberToUpdate.ReturnType == "System.Void") - { - ProblematicAPIs.AddIfNotExists($"Unexpected System.Void return value in Method=[{dMemberToUpdate.DocId}]"); - } - else if (tsMemberToPort != null && !IsEmpty(tsMemberToPort.Returns)) - { - name = tsMemberToPort.Name; - value = tsMemberToPort.Returns; - } - else if (interfacedMember != null && !IsEmpty(interfacedMember.Returns)) - { - name = interfacedMember.MemberName; - value = interfacedMember.Returns; - isEII = true; - } - - if (!IsEmpty(value)) - { - dMemberToUpdate.Returns = value; - string message = $"Method {GetIsEII(isEII)} returns {name.Escaped()} = {value.Escaped()}"; - PrintModifiedMember(message, dMemberToUpdate.FilePath, dMemberToUpdate.DocId); - TotalModifiedIndividualElements++; - } - } - } - - // Ports all the exceptions for the specified API. - // They are only processed if the user specified in the command arguments to NOT skip exceptions. - // All exceptions get ported, because there is no easy way to determine if an exception is already documented or not. - private void TryPortMissingExceptionsForMember(DocsMember dMemberToUpdate, TripleSlashMember? tsMemberToPort) - { - if (!Config.PortExceptionsExisting && !Config.PortExceptionsNew) - { - return; - } - - if (tsMemberToPort != null) - { - // Exceptions are a special case: If a new one is found in code, but does not exist in docs, the whole element needs to be added - foreach (TripleSlashException tsException in tsMemberToPort.Exceptions) - { - DocsException? dException = dMemberToUpdate.Exceptions.FirstOrDefault(x => x.Cref == tsException.Cref); - bool created = false; - - // First time adding the cref - if (dException == null && Config.PortExceptionsNew) - { - AddedExceptions.AddIfNotExists($"Exception=[{tsException.Cref}] in Member=[{dMemberToUpdate.DocId}]"); - string text = XmlHelper.ReplaceExceptionPatterns(XmlHelper.GetNodesInPlainText(tsException.XEException)); - dException = dMemberToUpdate.AddException(tsException.Cref, text); - created = true; - } - // If cref exists, check if the text has already been appended - else if (dException != null && Config.PortExceptionsExisting) - { - XElement formattedException = tsException.XEException; - string value = XmlHelper.ReplaceExceptionPatterns(XmlHelper.GetNodesInPlainText(formattedException)); - if (!dException.WordCountCollidesAboveThreshold(value, Config.ExceptionCollisionThreshold)) - { - AddedExceptions.AddIfNotExists($"Exception=[{tsException.Cref}] in Member=[{dMemberToUpdate.DocId}]"); - dException.AppendException(value); - created = true; - } - } - - if (dException != null) - { - if (created || (!IsEmpty(tsException.Value) && IsEmpty(dException.Value))) - { - string message = string.Format($"Exception ({GetIsCreated(created)}) {dException.Cref.Escaped()} = {dException.Value.Escaped()}"); - PrintModifiedMember(message, dException.ParentAPI.FilePath, dException.Cref); - - TotalModifiedIndividualElements++; - } - } - } - } - } - - // If a Param is found in a DocsType or a DocsMember that did not exist in the Triple Slash member, it's possible the param was unexpectedly saved in the triple slash comments with a different name, so the user gets prompted to look for it. - private bool TryPromptParam(DocsParam oldDParam, TripleSlashMember tsMember, out TripleSlashParam? newTsParam) - { - newTsParam = null; - - if (Config.DisablePrompts) - { - Log.Error($"Prompts disabled. Will not process the '{oldDParam.Name}' param."); - return false; - } - - bool created = false; - int option = -1; - while (option == -1) - { - Log.Error($"Problem in param '{oldDParam.Name}' in member '{tsMember.Name}' in file '{oldDParam.ParentAPI.FilePath}'"); - Log.Error($"The param probably exists in code, but the exact name was not found in Docs. What would you like to do?"); - Log.Warning(" 0 - Exit program."); - Log.Info(" 1 - Select the correct triple slash param from the existing ones."); - Log.Info(" 2 - Ignore this param."); - Log.Warning(" Note:Make sure to double check the affected Docs file after the tool finishes executing."); - Log.Cyan(false, "Your answer [0,1,2]: "); - - if (!int.TryParse(Console.ReadLine(), out option)) - { - Log.Error("Not a number. Try again."); - option = -1; - } - else - { - switch (option) - { - case 0: - { - Log.Info("Goodbye!"); - Environment.Exit(0); - break; - } - - case 1: - { - int paramSelection = -1; - while (paramSelection == -1) - { - Log.Info($"Triple slash params found in member '{tsMember.Name}':"); - Log.Warning(" 0 - Exit program."); - int paramCounter = 1; - foreach (TripleSlashParam param in tsMember.Params) - { - Log.Info($" {paramCounter} - {param.Name}"); - paramCounter++; - } - - Log.Cyan(false, $"Your answer to match param '{oldDParam.Name}'? [0..{paramCounter - 1}]: "); - - if (!int.TryParse(Console.ReadLine(), out paramSelection)) - { - Log.Error("Not a number. Try again."); - paramSelection = -1; - } - else if (paramSelection < 0 || paramSelection >= paramCounter) - { - Log.Error("Invalid selection. Try again."); - paramSelection = -1; - } - else if (paramSelection == 0) - { - Log.Info("Goodbye!"); - Environment.Exit(0); - } - else - { - newTsParam = tsMember.Params[paramSelection - 1]; - Log.Success($"Selected: {newTsParam.Name}"); - } - } - - break; - } - - case 2: - { - Log.Info("Skipping this param."); - break; - } - - default: - { - Log.Error("Invalid selection. Try again."); - option = -1; - break; - } - } - } - } - - return created; - } - - /// - /// Standard formatted print message for a modified element. - /// - /// The friendly description of the modified API. - /// The file where the modified API lives. - /// The API name in the triple slash file. - /// The API name in the Docs file. - /// The value that was found in the triple slash file. - /// The value that was found in the Docs file. - private void PrintModifiedMember(string message, string docsFilePath, string docId) - { - Log.Warning($" File: {docsFilePath}"); - Log.Warning($" DocID: {docId}"); - Log.Warning($" {message}"); - Log.Info("---------------------------------------------------"); - Log.Line(); - } - - // Prints all the undocumented APIs. - // This is only done if the user specified in the command arguments to print undocumented APIs. - private void PrintUndocumentedAPIs() - { - if (Config.PrintUndoc) - { - Log.Line(); - Log.Success("-----------------"); - Log.Success("UNDOCUMENTED APIS"); - Log.Success("-----------------"); - - Log.Line(); - - static void TryPrintType(ref bool undocAPI, string typeDocId) - { - if (!undocAPI) - { - Log.Info(" Type: {0}", typeDocId); - undocAPI = true; - } - }; - - static void TryPrintMember(ref bool undocMember, string memberDocId) - { - if (!undocMember) - { - Log.Info(" {0}", memberDocId); - undocMember = true; - } - }; - - int typeSummaries = 0; - int memberSummaries = 0; - int memberValues = 0; - int memberReturns = 0; - int memberParams = 0; - int memberTypeParams = 0; - int exceptions = 0; - - Log.Info("Undocumented APIs:"); - foreach (DocsType docsType in DocsComments.Types) - { - bool undocAPI = false; - if (IsEmpty(docsType.Summary)) - { - TryPrintType(ref undocAPI, docsType.DocId); - Log.Error($" Type Summary: {docsType.Summary}"); - typeSummaries++; - } - } - - foreach (DocsMember member in DocsComments.Members) - { - bool undocMember = false; - - if (IsEmpty(member.Summary)) - { - TryPrintMember(ref undocMember, member.DocId); - - Log.Error($" Member Summary: {member.Summary}"); - memberSummaries++; - } - - if (member.MemberType == "Property") - { - if (member.Value == Configuration.ToBeAdded) - { - TryPrintMember(ref undocMember, member.DocId); - - Log.Error($" Property Value: {member.Value}"); - memberValues++; - } - } - else if (member.MemberType == "Method") - { - if (member.Returns == Configuration.ToBeAdded) - { - TryPrintMember(ref undocMember, member.DocId); - - Log.Error($" Method Returns: {member.Returns}"); - memberReturns++; - } - } - - foreach (DocsParam param in member.Params) - { - if (IsEmpty(param.Value)) - { - TryPrintMember(ref undocMember, member.DocId); - - Log.Error($" Member Param: {param.Name}: {param.Value}"); - memberParams++; - } - } - - foreach (DocsTypeParam typeParam in member.TypeParams) - { - if (IsEmpty(typeParam.Value)) - { - TryPrintMember(ref undocMember, member.DocId); - - Log.Error($" Member Type Param: {typeParam.Name}: {typeParam.Value}"); - memberTypeParams++; - } - } - - foreach (DocsException exception in member.Exceptions) - { - if (IsEmpty(exception.Value)) - { - TryPrintMember(ref undocMember, member.DocId); - - Log.Error($" Member Exception: {exception.Cref}: {exception.Value}"); - exceptions++; - } - } - } - - Log.Info($" Undocumented type summaries: {typeSummaries}"); - Log.Info($" Undocumented member summaries: {memberSummaries}"); - Log.Info($" Undocumented method returns: {memberReturns}"); - Log.Info($" Undocumented property values: {memberValues}"); - Log.Info($" Undocumented member params: {memberParams}"); - Log.Info($" Undocumented member type params: {memberTypeParams}"); - Log.Info($" Undocumented exceptions: {exceptions}"); - - Log.Line(); - } - } - - // Prints a final summary of the execution findings. - private void PrintSummary() - { - Log.Line(); - Log.Success("---------"); - Log.Success("FINISHED!"); - Log.Success("---------"); - - Log.Line(); - Log.Info($"Total modified files: {ModifiedFiles.Count}"); - foreach (string file in ModifiedFiles) - { - Log.Success($" - {file}"); - } - - Log.Line(); - Log.Info($"Total modified types: {ModifiedTypes.Count}"); - foreach (string type in ModifiedTypes) - { - Log.Success($" - {type}"); - } - - Log.Line(); - Log.Info($"Total modified APIs: {ModifiedAPIs.Count}"); - foreach (string api in ModifiedAPIs) - { - Log.Success($" - {api}"); - } - - Log.Line(); - Log.Info($"Total problematic APIs: {ProblematicAPIs.Count}"); - foreach (string api in ProblematicAPIs) - { - Log.Warning($" - {api}"); - } - - Log.Line(); - Log.Info($"Total added exceptions: {AddedExceptions.Count}"); - foreach (string exception in AddedExceptions) - { - Log.Success($" - {exception}"); - } - - Log.Line(); - Log.Info(false, "Total modified individual elements: "); - Log.Success($"{TotalModifiedIndividualElements}"); - } - } -} diff --git a/DocsPortingTool/Configuration.cs b/DocsPortingTool/Configuration.cs deleted file mode 100644 index d48c7d5..0000000 --- a/DocsPortingTool/Configuration.cs +++ /dev/null @@ -1,575 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace DocsPortingTool -{ - public class Configuration - { - private static readonly char Separator = ','; - - private enum Mode - { - DisablePrompts, - Docs, - ExceptionCollisionThreshold, - ExcludedAssemblies, - ExcludedNamespaces, - ExcludedTypes, - IncludedAssemblies, - IncludedNamespaces, - IncludedTypes, - Initial, - PortExceptionsExisting, - PortExceptionsNew, - PortMemberParams, - PortMemberProperties, - PortMemberReturns, - PortMemberRemarks, - PortMemberSummaries, - PortMemberTypeParams, - PortTypeParams, // Params of a Type - PortTypeRemarks, - PortTypeSummaries, - PortTypeTypeParams, // TypeParams of a Type - PrintUndoc, - Save, - SkipInterfaceImplementations, - SkipInterfaceRemarks, - TripleSlash - } - - public static readonly string ToBeAdded = "To be added."; - - public static readonly string[] ForbiddenDirectories = new[] { "binplacePackages", "docs", "mscorlib", "native", "netfx", "netstandard", "pkg", "Product", "ref", "runtime", "shimsTargetRuntime", "testhost", "tests", "winrt" }; - - public List DirsTripleSlashXmls { get; } = new List(); - public List DirsDocsXml { get; } = new List(); - - public HashSet IncludedAssemblies { get; } = new HashSet(); - public HashSet ExcludedAssemblies { get; } = new HashSet(); - public HashSet IncludedNamespaces { get; } = new HashSet(); - public HashSet ExcludedNamespaces { get; } = new HashSet(); - public HashSet IncludedTypes { get; } = new HashSet(); - public HashSet ExcludedTypes { get; } = new HashSet(); - - public bool DisablePrompts { get; set; } = false; - public int ExceptionCollisionThreshold { get; set; } = 70; - public bool PortExceptionsExisting { get; set; } = false; - public bool PortExceptionsNew { get; set; } = true; - public bool PortMemberParams { get; set; } = true; - public bool PortMemberProperties { get; set; } = true; - public bool PortMemberReturns { get; set; } = true; - public bool PortMemberRemarks { get; set; } = true; - public bool PortMemberSummaries { get; set; } = true; - public bool PortMemberTypeParams { get; set; } = true; - /// - /// Params of a Type. - /// - public bool PortTypeParams { get; set; } = true; - public bool PortTypeRemarks { get; set; } = true; - public bool PortTypeSummaries { get; set; } = true; - /// - /// TypeParams of a Type. - /// - public bool PortTypeTypeParams { get; set; } = true; - public bool PrintUndoc { get; set; } = false; - public bool Save { get; set; } = false; - public bool SkipInterfaceImplementations { get; set; } = false; - public bool SkipInterfaceRemarks { get; set; } = true; - - public static Configuration GetFromCommandLineArguments(string[] args) - { - Mode mode = Mode.Initial; - - Log.Info("Verifying CLI arguments..."); - - if (args == null || args.Length == 0) - { - Log.LogErrorPrintHelpAndExit("No arguments passed to the executable."); - } - - Configuration config = new Configuration(); - - foreach (string arg in args!) - { - switch (mode) - { - case Mode.DisablePrompts: - { - config.DisablePrompts = ParseOrExit(arg, "Disable prompts"); - mode = Mode.Initial; - break; - } - - case Mode.Docs: - { - string[] splittedDirPaths = arg.Split(',', StringSplitOptions.RemoveEmptyEntries); - - Log.Cyan($"Specified Docs xml locations:"); - foreach (string dirPath in splittedDirPaths) - { - DirectoryInfo dirInfo = new DirectoryInfo(dirPath); - if (!dirInfo.Exists) - { - Log.LogErrorAndExit($"This Docs xml directory does not exist: {dirPath}"); - } - - config.DirsDocsXml.Add(dirInfo); - Log.Info($" - {dirPath}"); - } - - mode = Mode.Initial; - break; - - } - - case Mode.ExceptionCollisionThreshold: - { - if (!int.TryParse(arg, out int value)) - { - Log.LogErrorAndExit($"Invalid int value for 'Exception collision threshold' argument: {arg}"); - } - else if (value < 1 || value > 100) - { - Log.LogErrorAndExit($"Value needs to be between 0 and 100: {value}"); - } - - config.ExceptionCollisionThreshold = value; - - Log.Cyan($"Exception collision threshold:"); - Log.Info($" - {value}"); - break; - } - - case Mode.ExcludedAssemblies: - { - string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); - - if (splittedArg.Length > 0) - { - Log.Cyan("Excluded assemblies:"); - foreach (string assembly in splittedArg) - { - Log.Cyan($" - {assembly}"); - config.ExcludedAssemblies.Add(assembly); - } - } - else - { - Log.LogErrorPrintHelpAndExit("You must specify at least one assembly."); - } - - mode = Mode.Initial; - break; - } - - case Mode.ExcludedNamespaces: - { - string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); - - if (splittedArg.Length > 0) - { - Log.Cyan("Excluded namespaces:"); - foreach (string ns in splittedArg) - { - Log.Cyan($" - {ns}"); - config.ExcludedNamespaces.Add(ns); - } - } - else - { - Log.LogErrorPrintHelpAndExit("You must specify at least one namespace."); - } - - mode = Mode.Initial; - break; - } - - case Mode.ExcludedTypes: - { - string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); - - if (splittedArg.Length > 0) - { - Log.Cyan($"Excluded types:"); - foreach (string typeName in splittedArg) - { - Log.Cyan($" - {typeName}"); - config.ExcludedTypes.Add(typeName); - } - } - else - { - Log.LogErrorPrintHelpAndExit("You must specify at least one type name."); - } - - mode = Mode.Initial; - break; - } - - case Mode.IncludedAssemblies: - { - string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); - - if (splittedArg.Length > 0) - { - Log.Cyan($"Included assemblies:"); - foreach (string assembly in splittedArg) - { - Log.Cyan($" - {assembly}"); - config.IncludedAssemblies.Add(assembly); - } - } - else - { - Log.LogErrorPrintHelpAndExit("You must specify at least one assembly."); - } - - mode = Mode.Initial; - break; - } - - case Mode.IncludedNamespaces: - { - string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); - - if (splittedArg.Length > 0) - { - Log.Cyan($"Included namespaces:"); - foreach (string ns in splittedArg) - { - Log.Cyan($" - {ns}"); - config.IncludedNamespaces.Add(ns); - } - } - else - { - Log.LogErrorPrintHelpAndExit("You must specify at least one namespace."); - } - - mode = Mode.Initial; - break; - } - - case Mode.IncludedTypes: - { - string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); - - if (splittedArg.Length > 0) - { - Log.Cyan($"Included types:"); - foreach (string typeName in splittedArg) - { - Log.Cyan($" - {typeName}"); - config.IncludedTypes.Add(typeName); - } - } - else - { - Log.LogErrorPrintHelpAndExit("You must specify at least one type name."); - } - - mode = Mode.Initial; - break; - } - - case Mode.Initial: - { - switch (arg.ToUpperInvariant()) - { - case "-DOCS": - mode = Mode.Docs; - break; - - case "-DISABLEPROMPTS": - mode = Mode.DisablePrompts; - break; - - case "EXCEPTIONCOLLISIONTHRESHOLD": - mode = Mode.ExceptionCollisionThreshold; - break; - - case "-EXCLUDEDASSEMBLIES": - mode = Mode.ExcludedAssemblies; - break; - - case "-EXCLUDEDNAMESPACES": - mode = Mode.ExcludedNamespaces; - break; - - case "-EXCLUDEDTYPES": - mode = Mode.ExcludedTypes; - break; - - case "-H": - case "-HELP": - Log.PrintHelp(); - Environment.Exit(0); - break; - - case "-INCLUDEDASSEMBLIES": - mode = Mode.IncludedAssemblies; - break; - - case "-INCLUDEDNAMESPACES": - mode = Mode.IncludedNamespaces; - break; - - case "-INCLUDEDTYPES": - mode = Mode.IncludedTypes; - break; - - case "-PORTEXCEPTIONSEXISTING": - mode = Mode.PortExceptionsExisting; - break; - - case "-PORTEXCEPTIONSNEW": - mode = Mode.PortExceptionsNew; - break; - - case "-PORTMEMBERPARAMS": - mode = Mode.PortMemberParams; - break; - - case "-PORTMEMBERPROPERTIES": - mode = Mode.PortMemberProperties; - break; - - case "-PORTMEMBERRETURNS": - mode = Mode.PortMemberReturns; - break; - - case "-PORTMEMBERREMARKS": - mode = Mode.PortMemberRemarks; - break; - - case "-PORTMEMBERSUMMARIES": - mode = Mode.PortMemberSummaries; - break; - - case "-PORTMEMBERTYPEPARAMS": - mode = Mode.PortMemberTypeParams; - break; - - case "-PORTTYPEPARAMS": // Params of a Type - mode = Mode.PortTypeParams; - break; - - case "-PORTTYPEREMARKS": - mode = Mode.PortTypeRemarks; - break; - - case "-PORTTYPESUMMARIES": - mode = Mode.PortTypeSummaries; - break; - - case "-PORTTYPETYPEPARAMS": // TypeParams of a Type - mode = Mode.PortTypeTypeParams; - break; - - case "-PRINTUNDOC": - mode = Mode.PrintUndoc; - break; - - case "-SAVE": - mode = Mode.Save; - break; - - case "-SKIPINTERFACEIMPLEMENTATIONS": - mode = Mode.SkipInterfaceImplementations; - break; - - case "-SKIPINTERFACEREMARKS": - mode = Mode.SkipInterfaceRemarks; - break; - - case "-TRIPLESLASH": - mode = Mode.TripleSlash; - break; - default: - Log.LogErrorPrintHelpAndExit($"Unrecognized argument: {arg}"); - break; - } - break; - } - - case Mode.PortExceptionsExisting: - { - config.PortExceptionsExisting = ParseOrExit(arg, "Port existing exceptions"); - mode = Mode.Initial; - break; - } - - case Mode.PortExceptionsNew: - { - config.PortExceptionsNew = ParseOrExit(arg, "Port new exceptions"); - mode = Mode.Initial; - break; - } - - case Mode.PortMemberParams: - { - config.PortMemberParams = ParseOrExit(arg, "Port member Params"); - mode = Mode.Initial; - break; - } - - case Mode.PortMemberProperties: - { - config.PortMemberProperties = ParseOrExit(arg, "Port member Properties"); - mode = Mode.Initial; - break; - } - - case Mode.PortMemberRemarks: - { - config.PortMemberRemarks = ParseOrExit(arg, "Port member Remarks"); - mode = Mode.Initial; - break; - } - - case Mode.PortMemberReturns: - { - config.PortMemberReturns = ParseOrExit(arg, "Port member Returns"); - mode = Mode.Initial; - break; - } - - case Mode.PortMemberSummaries: - { - config.PortMemberSummaries = ParseOrExit(arg, "Port member Summaries"); - mode = Mode.Initial; - break; - } - - case Mode.PortMemberTypeParams: - { - config.PortMemberTypeParams = ParseOrExit(arg, "Port member TypeParams"); - mode = Mode.Initial; - break; - } - - case Mode.PortTypeParams: // Params of a Type - { - config.PortTypeParams = ParseOrExit(arg, "Port Type Params"); - mode = Mode.Initial; - break; - } - - case Mode.PortTypeRemarks: - { - config.PortTypeRemarks = ParseOrExit(arg, "Port Type Remarks"); - mode = Mode.Initial; - break; - } - - case Mode.PortTypeSummaries: - { - config.PortTypeSummaries = ParseOrExit(arg, "Port Type Summaries"); - mode = Mode.Initial; - break; - } - - case Mode.PortTypeTypeParams: // TypeParams of a Type - { - config.PortTypeTypeParams = ParseOrExit(arg, "Port Type TypeParams"); - mode = Mode.Initial; - break; - } - - case Mode.PrintUndoc: - { - config.PrintUndoc = ParseOrExit(arg, "Print undoc"); - mode = Mode.Initial; - break; - } - - case Mode.Save: - { - config.Save = ParseOrExit(arg, "Save"); - mode = Mode.Initial; - break; - } - - case Mode.SkipInterfaceImplementations: - { - config.SkipInterfaceImplementations = ParseOrExit(arg, "Skip interface implementations"); - mode = Mode.Initial; - break; - } - - case Mode.SkipInterfaceRemarks: - { - config.SkipInterfaceRemarks = ParseOrExit(arg, "Skip appending interface remarks"); - mode = Mode.Initial; - break; - } - - case Mode.TripleSlash: - { - string[] splittedDirPaths = arg.Split(',', StringSplitOptions.RemoveEmptyEntries); - - Log.Cyan($"Specified triple slash locations:"); - foreach (string dirPath in splittedDirPaths) - { - DirectoryInfo dirInfo = new DirectoryInfo(dirPath); - if (!dirInfo.Exists) - { - Log.LogErrorAndExit($"This triple slash xml directory does not exist: {dirPath}"); - } - - config.DirsTripleSlashXmls.Add(dirInfo); - Log.Info($" - {dirPath}"); - } - - mode = Mode.Initial; - break; - } - - default: - { - Log.LogErrorPrintHelpAndExit("Unexpected mode."); - break; - } - } - } - - if (mode != Mode.Initial) - { - Log.LogErrorPrintHelpAndExit("You missed an argument value."); - } - - if (config.DirsDocsXml == null) - { - Log.LogErrorPrintHelpAndExit($"You must specify a path to the dotnet-api-docs xml folder with {nameof(Docs)}."); - } - - if (config.DirsTripleSlashXmls.Count == 0) - { - Log.LogErrorPrintHelpAndExit($"You must specify at least one triple slash xml folder path with {nameof(TripleSlash)}."); - } - - if (config.IncludedAssemblies.Count == 0) - { - Log.LogErrorPrintHelpAndExit($"You must specify at least one assembly with {nameof(IncludedAssemblies)}."); - } - - return config; - } - - // Tries to parse the user argument string as boolean, and if it fails, exits the program. - private static bool ParseOrExit(string arg, string paramFriendlyName) - { - if (!bool.TryParse(arg, out bool value)) - { - Log.LogErrorAndExit($"Invalid boolean value for '{paramFriendlyName}' argument: {arg}"); - } - - Log.Cyan($"{paramFriendlyName}:"); - Log.Info($" - {value}"); - - return value; - } - } -} diff --git a/DocsPortingTool/Docs/APIKind.cs b/DocsPortingTool/Docs/APIKind.cs deleted file mode 100644 index 5c2d7c9..0000000 --- a/DocsPortingTool/Docs/APIKind.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace DocsPortingTool.Docs -{ - public enum APIKind - { - Type, - Member - } -} diff --git a/DocsPortingTool/Docs/DocsAPI.cs b/DocsPortingTool/Docs/DocsAPI.cs deleted file mode 100644 index 9b7b19f..0000000 --- a/DocsPortingTool/Docs/DocsAPI.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public abstract class DocsAPI : IDocsAPI - { - private string? _docIdEscaped = null; - private List? _params; - private List? _parameters; - private List? _typeParameters; - private List? _typeParams; - private List? _assemblyInfos; - - protected readonly XElement XERoot; - - protected DocsAPI(XElement xeRoot) => XERoot = xeRoot; - - public abstract bool Changed { get; set; } - public string FilePath { get; set; } = string.Empty; - public abstract string DocId { get; } - - /// - /// The Parameter elements found inside the Parameters section. - /// - public List Parameters - { - get - { - if (_parameters == null) - { - XElement xeParameters = XERoot.Element("Parameters"); - if (xeParameters != null) - { - _parameters = xeParameters.Elements("Parameter").Select(x => new DocsParameter(x)).ToList(); - } - else - { - _parameters = new List(); - } - } - return _parameters; - } - } - - /// - /// The TypeParameter elements found inside the TypeParameters section. - /// - public List TypeParameters - { - get - { - if (_typeParameters == null) - { - XElement xeTypeParameters = XERoot.Element("TypeParameters"); - if (xeTypeParameters != null) - { - _typeParameters = xeTypeParameters.Elements("TypeParameter").Select(x => new DocsTypeParameter(x)).ToList(); - } - else - { - _typeParameters = new List(); - } - } - return _typeParameters; - } - } - - public XElement Docs - { - get - { - return XERoot.Element("Docs"); - } - } - - /// - /// 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; - } - } - - /// - /// 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 List(); - } - } - return _typeParams; - } - } - - public abstract string Summary { get; set; } - - public abstract string Remarks { get; set; } - - public List AssemblyInfos - { - get - { - if (_assemblyInfos == null) - { - _assemblyInfos = new List(); - } - return _assemblyInfos; - } - } - - public string DocIdEscaped - { - get - { - if (_docIdEscaped == null) - { - _docIdEscaped = DocId.Replace("<", "{").Replace(">", "}").Replace("<", "{").Replace(">", "}"); - } - return _docIdEscaped; - } - } - - public DocsParam SaveParam(XElement xeTripleSlashParam) - { - XElement xeDocsParam = new XElement(xeTripleSlashParam.Name); - xeDocsParam.ReplaceAttributes(xeTripleSlashParam.Attributes()); - XmlHelper.SaveFormattedAsXml(xeDocsParam, xeTripleSlashParam.Value); - DocsParam docsParam = new DocsParam(this, xeDocsParam); - Changed = true; - return docsParam; - } - - public APIKind Kind - { - get - { - return this switch - { - DocsMember _ => APIKind.Member, - DocsType _ => APIKind.Type, - _ => throw new ArgumentException("Unrecognized IDocsAPI object") - }; - } - } - - public DocsTypeParam AddTypeParam(string name, string value) - { - XElement typeParam = new XElement("typeparam"); - typeParam.SetAttributeValue("name", name); - XmlHelper.AddChildFormattedAsXml(Docs, typeParam, value); - Changed = true; - return new DocsTypeParam(this, typeParam); - } - - 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; - } - } - - // 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) - { - element = Docs.Element(name); - - if (element == null && addIfMissing) - { - element = new XElement(name); - XmlHelper.AddChildFormattedAsXml(Docs, element, Configuration.ToBeAdded); - } - - return element != null; - } - } -} diff --git a/DocsPortingTool/Docs/DocsAssemblyInfo.cs b/DocsPortingTool/Docs/DocsAssemblyInfo.cs deleted file mode 100644 index 81f4180..0000000 --- a/DocsPortingTool/Docs/DocsAssemblyInfo.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public class DocsAssemblyInfo - { - private readonly XElement XEAssemblyInfo; - public string AssemblyName - { - get - { - return 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 DocsAssemblyInfo(XElement xeAssemblyInfo) - { - XEAssemblyInfo = xeAssemblyInfo; - } - - public override string ToString() => AssemblyName; - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsAttribute.cs b/DocsPortingTool/Docs/DocsAttribute.cs deleted file mode 100644 index b4111b4..0000000 --- a/DocsPortingTool/Docs/DocsAttribute.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public 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 DocsAttribute(XElement xeAttribute) - { - XEAttribute = xeAttribute; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsCommentsContainer.cs b/DocsPortingTool/Docs/DocsCommentsContainer.cs deleted file mode 100644 index 6bc2ab8..0000000 --- a/DocsPortingTool/Docs/DocsCommentsContainer.cs +++ /dev/null @@ -1,303 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Xml; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public class DocsCommentsContainer - { - private Configuration Config { get; set; } - - private XDocument? xDoc = null; - - public readonly List Types = new List(); - public readonly List Members = new List(); - - public DocsCommentsContainer(Configuration config) - { - Config = config; - } - - public void CollectFiles() - { - Log.Info("Looking for Docs xml files..."); - - foreach (FileInfo fileInfo in EnumerateFiles()) - { - LoadFile(fileInfo); - } - - Log.Success("Finished looking for Docs xml files."); - Log.Line(); - } - - public void Save() - { - if (!Config.Save) - { - Log.Line(); - Log.Error("[No files were saved]"); - Log.Warning($"Did you forget to specify '-{nameof(Config.Save)} true'?"); - Log.Line(); - - return; - } - - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - Encoding encoding = Encoding.GetEncoding(1252); // Preserves original xml encoding from Docs repo - - List savedFiles = new List(); - foreach (var type in Types.Where(x => x.Changed)) - { - Log.Warning(false, $"Saving changes for {type.FilePath}:"); - - try - { - StreamReader sr = new StreamReader(type.FilePath); - int x = sr.Read(); // Force the first read to be done so the encoding is detected - sr.Close(); - - // These settings prevent the addition of the element on the first line and will preserve indentation+endlines - XmlWriterSettings xws = new XmlWriterSettings - { - OmitXmlDeclaration = true, - Indent = true, - Encoding = encoding, - CheckCharacters = false - }; - - using (XmlWriter xw = XmlWriter.Create(type.FilePath, xws)) - { - type.XDoc.Save(xw); - } - - // Workaround to delete the annoying endline added by XmlWriter.Save - string fileData = File.ReadAllText(type.FilePath); - if (!fileData.EndsWith(Environment.NewLine)) - { - File.WriteAllText(type.FilePath, fileData + Environment.NewLine, encoding); - } - - Log.Success(" [Saved]"); - } - catch (Exception e) - { - Log.Error(e.Message); - Log.Line(); - Log.Error(e.StackTrace ?? string.Empty); - if (e.InnerException != null) - { - Log.Line(); - Log.Error(e.InnerException.Message); - Log.Line(); - Log.Error(e.InnerException.StackTrace ?? string.Empty); - } - System.Threading.Thread.Sleep(1000); - } - - Log.Line(); - } - } - - private bool HasAllowedDirName(DirectoryInfo dirInfo) - { - return !Configuration.ForbiddenDirectories.Contains(dirInfo.Name) && !dirInfo.Name.EndsWith(".Tests"); - } - - private bool HasAllowedFileName(FileInfo fileInfo) - { - return !fileInfo.Name.StartsWith("ns-") && - fileInfo.Name != "index.xml" && - fileInfo.Name != "_filter.xml"; - } - - private IEnumerable EnumerateFiles() - { - var includedAssembliesAndNamespaces = Config.IncludedAssemblies.Concat(Config.IncludedNamespaces); - var excludedAssembliesAndNamespaces = Config.ExcludedAssemblies.Concat(Config.ExcludedNamespaces); - - foreach (DirectoryInfo rootDir in Config.DirsDocsXml) - { - // Try to find folders with the names of assemblies AND namespaces (if the user specified any) - foreach (string included in includedAssembliesAndNamespaces) - { - // If the user specified a sub-assembly or sub-namespace to exclude, we need to skip it - if (excludedAssembliesAndNamespaces.Any(excluded => included.StartsWith(excluded))) - { - continue; - } - - foreach (DirectoryInfo subDir in rootDir.EnumerateDirectories($"{included}*", SearchOption.TopDirectoryOnly)) - { - if (HasAllowedDirName(subDir)) - { - foreach (FileInfo fileInfo in subDir.EnumerateFiles("*.xml", SearchOption.AllDirectories)) - { - if (HasAllowedFileName(fileInfo)) - { - // LoadFile will determine if the Type is allowed or not - yield return fileInfo; - } - } - } - } - - if (!Config.SkipInterfaceImplementations) - { - // Find interfaces only inside System.* folders. - // Including Microsoft.* folders reaches the max limit of files to include in a list, plus there are no essential interfaces there. - foreach (DirectoryInfo subDir in rootDir.EnumerateDirectories("System*", SearchOption.AllDirectories)) - { - if (!Configuration.ForbiddenDirectories.Contains(subDir.Name) && - // Exclude any folder that starts with the excluded assemblies OR excluded namespaces - !excludedAssembliesAndNamespaces.Any(excluded => subDir.Name.StartsWith(excluded)) && !subDir.Name.EndsWith(".Tests")) - { - // Ensure including interface files that start with I and then an uppercase letter, and prevent including files like 'Int' - foreach (FileInfo fileInfo in subDir.EnumerateFiles("I*.xml", SearchOption.AllDirectories)) - { - if (fileInfo.Name[1] >= 'A' || fileInfo.Name[1] <= 'Z') - { - yield return fileInfo; - } - } - } - } - } - } - } - } - - private void LoadFile(FileInfo fileInfo) - { - if (!fileInfo.Exists) - { - Log.Error($"Docs xml file does not exist: {fileInfo.FullName}"); - return; - } - - xDoc = XDocument.Load(fileInfo.FullName); - - if (IsXmlMalformed(xDoc, fileInfo.FullName)) - { - return; - } - - DocsType docsType = new DocsType(fileInfo.FullName, xDoc, xDoc.Root); - - bool add = false; - bool addedAsInterface = false; - - bool containsForbiddenAssembly = docsType.AssemblyInfos.Any(assemblyInfo => - Config.ExcludedAssemblies.Any(excluded => assemblyInfo.AssemblyName.StartsWith(excluded)) || - Config.ExcludedNamespaces.Any(excluded => assemblyInfo.AssemblyName.StartsWith(excluded))); - - if (!containsForbiddenAssembly) - { - // If it's an interface, always add it if the user wants to detect EIIs, - // even if it's in an assembly that was not included but was not explicitly excluded - addedAsInterface = false; - if (!Config.SkipInterfaceImplementations) - { - // Interface files start with I, and have an 2nd alphabetic character - addedAsInterface = docsType.Name.Length >= 2 && docsType.Name[0] == 'I' && docsType.Name[1] >= 'A' && docsType.Name[1] <= 'Z'; - add |= addedAsInterface; - - } - - bool containsAllowedAssembly = docsType.AssemblyInfos.Any(assemblyInfo => - Config.IncludedAssemblies.Any(included => assemblyInfo.AssemblyName.StartsWith(included)) || - Config.IncludedNamespaces.Any(included => assemblyInfo.AssemblyName.StartsWith(included))); - - if (containsAllowedAssembly) - { - // If it was already added above as an interface, skip this part - // Otherwise, find out if the type belongs to the included assemblies, and if specified, to the included (and not excluded) types - // This includes interfaces even if user wants to skip EIIs - They will be added if they belong to this namespace or to the list of - // included (and not exluded) types, but will not be used for EII, but rather as normal types whose comments should be ported - if (!addedAsInterface) - { - // Either the user didn't specify namespace filtering (allow all namespaces) or specified particular ones to include/exclude - if (!Config.IncludedNamespaces.Any() || - (Config.IncludedNamespaces.Any(included => docsType.Namespace.StartsWith(included)) && - !Config.ExcludedNamespaces.Any(excluded => docsType.Namespace.StartsWith(excluded)))) - { - // Can add if the user didn't specify type filtering (allow all types), or specified particular ones to include/exclude - add = !Config.IncludedTypes.Any() || - (Config.IncludedTypes.Contains(docsType.Name) && - !Config.ExcludedTypes.Contains(docsType.Name)); - } - } - } - } - - if (add) - { - int totalMembersAdded = 0; - Types.Add(docsType); - - if (XmlHelper.TryGetChildElement(xDoc.Root, "Members", out XElement? xeMembers) && xeMembers != null) - { - foreach (XElement xeMember in xeMembers.Elements("Member")) - { - DocsMember member = new DocsMember(fileInfo.FullName, docsType, xeMember); - totalMembersAdded++; - Members.Add(member); - } - } - - string message = $"Type {docsType.DocId} added with {totalMembersAdded} member(s) included."; - if (addedAsInterface) - { - Log.Magenta("[Interface] - " + message); - } - else if (totalMembersAdded == 0) - { - Log.Warning(message); - } - else - { - Log.Success(message); - } - } - } - - private bool IsXmlMalformed(XDocument xDoc, string fileName) - { - if (xDoc.Root == null) - { - Log.Error($"Docs xml file does not have a root element: {fileName}"); - return true; - } - - if (xDoc.Root.Name == "Namespace") - { - Log.Error($"Skipping namespace file (should have been filtered already): {fileName}"); - return true; - } - - if (xDoc.Root.Name != "Type") - { - Log.Error($"Docs xml file does not have a 'Type' root element: {fileName}"); - return true; - } - - if (!xDoc.Root.HasElements) - { - Log.Error($"Docs xml file Type element does not have any children: {fileName}"); - return true; - } - - if (xDoc.Root.Elements("Docs").Count() != 1) - { - Log.Error($"Docs xml file Type element does not have a Docs child: {fileName}"); - return true; - } - - return false; - } - } -} diff --git a/DocsPortingTool/Docs/DocsException.cs b/DocsPortingTool/Docs/DocsException.cs deleted file mode 100644 index 68efc75..0000000 --- a/DocsPortingTool/Docs/DocsException.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public class DocsException - { - private readonly XElement XEException; - - public IDocsAPI ParentAPI - { - get; private set; - } - - public string Cref - { - get - { - return XmlHelper.GetAttributeValue(XEException, "cref"); - } - } - - public string Value - { - get - { - return XmlHelper.GetNodesInPlainText(XEException); - } - private set - { - XmlHelper.SaveFormattedAsXml(XEException, value); - } - } - - public string OriginalValue { get; private set; } - - public DocsException(IDocsAPI parentAPI, XElement xException) - { - ParentAPI = parentAPI; - XEException = 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 tripleSlashValue, int threshold) - { - Dictionary hashTripleSlash = GetHash(tripleSlashValue); - Dictionary hashDocs = GetHash(Value); - - int collisions = 0; - // Iterate all the words of the triple slash exception string - foreach (KeyValuePair word in hashTripleSlash) - { - // 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 triple slash - // 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)hashTripleSlash.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; - } - } -} diff --git a/DocsPortingTool/Docs/DocsMember.cs b/DocsPortingTool/Docs/DocsMember.cs deleted file mode 100644 index baedc1d..0000000 --- a/DocsPortingTool/Docs/DocsMember.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public class DocsMember : DocsAPI - { - private string? _memberName; - private List? _memberSignatures; - private string? _docId; - private List? _altMemberCref; - private List? _exceptions; - - public DocsMember(string filePath, DocsType parentType, XElement xeMember) - : base(xeMember) - { - FilePath = filePath; - ParentType = parentType; - AssemblyInfos.AddRange(XERoot.Elements("AssemblyInfo").Select(x => new DocsAssemblyInfo(x))); - } - - public DocsType ParentType { get; private set; } - - public override bool Changed - { - get => ParentType.Changed; - set => ParentType.Changed |= value; - } - - 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 override string DocId - { - get - { - if (_docId == null) - { - _docId = string.Empty; - DocsMemberSignature? ms = MemberSignatures.FirstOrDefault(x => x.Language == "DocId"); - if (ms == null) - { - string message = string.Format("Could not find a DocId MemberSignature for '{0}'", MemberName); - Log.Error(message); - throw new MissingMemberException(message); - } - _docId = ms.Value; - } - return _docId; - } - } - - public string MemberType - { - get - { - return XmlHelper.GetChildElementValue(XERoot, "MemberType"); - } - } - - public string ImplementsInterfaceMember - { - get - { - XElement xeImplements = XERoot.Element("Implements"); - if (xeImplements != null) - { - return XmlHelper.GetChildElementValue(xeImplements, "InterfaceMember"); - } - return string.Empty; - } - } - - public string ReturnType - { - get - { - XElement xeReturnValue = XERoot.Element("ReturnValue"); - if (xeReturnValue != null) - { - return XmlHelper.GetChildElementValue(xeReturnValue, "ReturnType"); - } - return string.Empty; - } - } - - public 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: {DocIdEscaped}"); - } - } - } - - public override string Summary - { - get - { - return GetNodesInPlainText("summary"); - } - set - { - SaveFormattedAsXml("summary", value, addIfMissing: true); - } - } - - public override string Remarks - { - get - { - return GetNodesInPlainText("remarks"); - } - set - { - SaveFormattedAsMarkdown("remarks", value, addIfMissing: !Analyzer.IsEmpty(value), isMember: true); - } - } - - 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: {DocIdEscaped}"); - } - } - } - - public List AltMemberCref - { - get - { - if (_altMemberCref == null) - { - if (Docs != null) - { - _altMemberCref = Docs.Elements("altmember").Select(x => XmlHelper.GetAttributeValue(x, "cref")).ToList(); - } - else - { - _altMemberCref = new List(); - } - } - return _altMemberCref; - } - } - - public List Exceptions - { - get - { - if (_exceptions == null) - { - if (Docs != null) - { - _exceptions = Docs.Elements("exception").Select(x => new DocsException(this, x)).ToList(); - } - else - { - _exceptions = new List(); - } - } - return _exceptions; - } - } - - public override string ToString() - { - return DocId; - } - - public DocsException AddException(string cref, string value) - { - XElement exception = new XElement("exception"); - exception.SetAttributeValue("cref", cref); - XmlHelper.AddChildFormattedAsXml(Docs, exception, value); - Changed = true; - return new DocsException(this, exception); - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsMemberSignature.cs b/DocsPortingTool/Docs/DocsMemberSignature.cs deleted file mode 100644 index 3fbdf66..0000000 --- a/DocsPortingTool/Docs/DocsMemberSignature.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public class DocsMemberSignature - { - private readonly XElement XEMemberSignature; - - public string Language - { - get - { - return XmlHelper.GetAttributeValue(XEMemberSignature, "Language"); - } - } - - public string Value - { - get - { - return XmlHelper.GetAttributeValue(XEMemberSignature, "Value"); - } - } - - public DocsMemberSignature(XElement xeMemberSignature) - { - XEMemberSignature = xeMemberSignature; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsParam.cs b/DocsPortingTool/Docs/DocsParam.cs deleted file mode 100644 index 7ec3a1a..0000000 --- a/DocsPortingTool/Docs/DocsParam.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public 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 DocsParam(IDocsAPI parentAPI, XElement xeDocsParam) - { - ParentAPI = parentAPI; - XEDocsParam = xeDocsParam; - } - public override string ToString() - { - return Name; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsParameter.cs b/DocsPortingTool/Docs/DocsParameter.cs deleted file mode 100644 index c9dd4bf..0000000 --- a/DocsPortingTool/Docs/DocsParameter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public 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; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsType.cs b/DocsPortingTool/Docs/DocsType.cs deleted file mode 100644 index 5628635..0000000 --- a/DocsPortingTool/Docs/DocsType.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - /// - /// Represents the root xml element (unique) of a Docs xml file, called Type. - /// - public class DocsType : DocsAPI - { - private string? _name; - private string? _fullName; - private string? _namespace; - private string? _docId; - private string? _baseTypeName; - private List? _interfaceNames; - private List? _attributes; - private List? _typesSignatures; - - public DocsType(string filePath, XDocument xDoc, XElement xeRoot) - : base(xeRoot) - { - FilePath = filePath; - XDoc = xDoc; - AssemblyInfos.AddRange(XERoot.Elements("AssemblyInfo").Select(x => new DocsAssemblyInfo(x))); - } - - public XDocument XDoc { get; set; } - - public override bool Changed { get; set; } - - public string Name - { - get - { - if (_name == null) - { - _name = XmlHelper.GetAttributeValue(XERoot, "Name"); - } - return _name; - } - } - - public string FullName - { - get - { - if (_fullName == null) - { - _fullName = XmlHelper.GetAttributeValue(XERoot, "FullName"); - } - return _fullName; - } - } - - public string Namespace - { - get - { - if (_namespace == null) - { - int lastDotPosition = FullName.LastIndexOf('.'); - _namespace = lastDotPosition < 0 ? FullName : FullName.Substring(0, lastDotPosition); - } - return _namespace; - } - } - - public List TypeSignatures - { - get - { - if (_typesSignatures == null) - { - _typesSignatures = XERoot.Elements("TypeSignature").Select(x => new DocsTypeSignature(x)).ToList(); - } - return _typesSignatures; - } - } - - public override string DocId - { - get - { - if (_docId == null) - { - DocsTypeSignature? dts = TypeSignatures.FirstOrDefault(x => x.Language == "DocId"); - if (dts == null) - { - string message = $"DocId TypeSignature not found for FullName"; - Log.Error($"DocId TypeSignature not found for FullName"); - throw new MissingMemberException(message); - } - _docId = dts.Value; - } - return _docId; - } - } - - public XElement Base - { - get - { - return XERoot.Element("Base"); - } - } - - public string BaseTypeName - { - get - { - if (_baseTypeName == null) - { - _baseTypeName = XmlHelper.GetChildElementValue(Base, "BaseTypeName"); - } - return _baseTypeName; - } - } - - public XElement Interfaces - { - get - { - return XERoot.Element("Interfaces"); - } - } - - public List InterfaceNames - { - get - { - if (_interfaceNames == null) - { - _interfaceNames = Interfaces.Elements("Interface").Select(x => XmlHelper.GetChildElementValue(x, "InterfaceName")).ToList(); - } - return _interfaceNames; - } - } - - public List Attributes - { - get - { - if (_attributes == null) - { - XElement e = XERoot.Element("Attributes"); - _attributes = (e != null) ? e.Elements("Attribute").Select(x => new DocsAttribute(x)).ToList() : new List(); - } - return _attributes; - } - } - - public override string Summary - { - get - { - return GetNodesInPlainText("summary"); - } - set - { - SaveFormattedAsXml("summary", value, addIfMissing: true); - } - } - - public override string Remarks - { - get - { - return GetNodesInPlainText("remarks"); - } - set - { - SaveFormattedAsMarkdown("remarks", value, addIfMissing: !Analyzer.IsEmpty(value), isMember: false); - } - } - - public override string ToString() - { - return FullName; - } - } -} diff --git a/DocsPortingTool/Docs/DocsTypeParam.cs b/DocsPortingTool/Docs/DocsTypeParam.cs deleted file mode 100644 index 08790e3..0000000 --- a/DocsPortingTool/Docs/DocsTypeParam.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Threading; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - /// - /// Each one of these typeparam objects live inside the Docs section inside the Member object. - /// - public class DocsTypeParam - { - private readonly XElement XEDocsTypeParam; - public IDocsAPI ParentAPI - { - get; private set; - } - public string Name - { - get - { - return XmlHelper.GetAttributeValue(XEDocsTypeParam, "name"); - } - } - public string Value - { - get - { - return XmlHelper.GetNodesInPlainText(XEDocsTypeParam); - } - set - { - XmlHelper.SaveFormattedAsXml(XEDocsTypeParam, value); - ParentAPI.Changed = true; - } - } - - public DocsTypeParam(IDocsAPI parentAPI, XElement xeDocsTypeParam) - { - ParentAPI = parentAPI; - XEDocsTypeParam = xeDocsTypeParam; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsTypeParameter.cs b/DocsPortingTool/Docs/DocsTypeParameter.cs deleted file mode 100644 index d32482e..0000000 --- a/DocsPortingTool/Docs/DocsTypeParameter.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - /// - /// Each one of these TypeParameter objects islocated inside the TypeParameters section inside the Member. - /// - public class DocsTypeParameter - { - private readonly XElement XETypeParameter; - public string Name - { - get - { - return XmlHelper.GetAttributeValue(XETypeParameter, "Name"); - } - } - private XElement? Constraints - { - get - { - return XETypeParameter.Element("Constraints"); - } - } - private List? _constraintsParamterAttributes; - public List ConstraintsParameterAttributes - { - get - { - if (_constraintsParamterAttributes == null) - { - if (Constraints != null) - { - _constraintsParamterAttributes = Constraints.Elements("ParameterAttribute").Select(x => XmlHelper.GetNodesInPlainText(x)).ToList(); - } - else - { - _constraintsParamterAttributes = new List(); - } - } - return _constraintsParamterAttributes; - } - } - - public string ConstraintsBaseTypeName - { - get - { - if (Constraints != null) - { - return XmlHelper.GetChildElementValue(Constraints, "BaseTypeName"); - } - return string.Empty; - } - } - - public DocsTypeParameter(XElement xeTypeParameter) - { - XETypeParameter = xeTypeParameter; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsTypeSignature.cs b/DocsPortingTool/Docs/DocsTypeSignature.cs deleted file mode 100644 index 97fa67c..0000000 --- a/DocsPortingTool/Docs/DocsTypeSignature.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public class DocsTypeSignature - { - private readonly XElement XETypeSignature; - - public string Language - { - get - { - return XmlHelper.GetAttributeValue(XETypeSignature, "Language"); - } - } - - public string Value - { - get - { - return XmlHelper.GetAttributeValue(XETypeSignature, "Value"); - } - } - - public DocsTypeSignature(XElement xeTypeSignature) - { - XETypeSignature = xeTypeSignature; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/IDocsAPI.cs b/DocsPortingTool/Docs/IDocsAPI.cs deleted file mode 100644 index 71cf16a..0000000 --- a/DocsPortingTool/Docs/IDocsAPI.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public interface IDocsAPI - { - public abstract APIKind Kind { get; } - public abstract bool Changed { get; set; } - public abstract string FilePath { get; set; } - public abstract string DocId { get; } - public abstract XElement Docs { get; } - public abstract List Parameters { get; } - public abstract List Params { get; } - public abstract List TypeParameters { get; } - public abstract List TypeParams { get; } - public abstract string Summary { get; set; } - public abstract string Remarks { get; set; } - public abstract DocsParam SaveParam(XElement xeCoreFXParam); - public abstract DocsTypeParam AddTypeParam(string name, string value); - } -} diff --git a/DocsPortingTool/DocsPortingTool.cs b/DocsPortingTool/DocsPortingTool.cs deleted file mode 100644 index 494a5a7..0000000 --- a/DocsPortingTool/DocsPortingTool.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DocsPortingTool -{ - public static class DocsPortingTool - { - public static void Main(string[] args) - { - Configuration config = Configuration.GetFromCommandLineArguments(args); - Analyzer analyzer = new Analyzer(config); - analyzer.Start(); - } - } -} diff --git a/DocsPortingTool/DocsPortingTool.csproj b/DocsPortingTool/DocsPortingTool.csproj deleted file mode 100644 index a75771d..0000000 --- a/DocsPortingTool/DocsPortingTool.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - Exe - net5.0 - - - Microsoft - carlossanlop - enable - true - true - - - - - - diff --git a/DocsPortingTool/Extensions.cs b/DocsPortingTool/Extensions.cs deleted file mode 100644 index fdcb90d..0000000 --- a/DocsPortingTool/Extensions.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; - -namespace DocsPortingTool -{ - // Provides generic extension methods. - public static class Extensions - { - // Adds a string to a list of strings if the element is not there yet. The method makes sure to escape unexpected curly brackets to prevent formatting exceptions. - public static void AddIfNotExists(this List list, string element) - { - string cleanedElement = element.Escaped(); - if (!list.Contains(cleanedElement)) - { - list.Add(cleanedElement); - } - } - - // Removes the specified subtrings from another string - public static string RemoveSubstrings(this string oldString, params string[] stringsToRemove) - { - string newString = oldString; - foreach (string toRemove in stringsToRemove) - { - if (newString.Contains(toRemove)) - { - newString = newString.Replace(toRemove, string.Empty); - } - } - return newString; - } - - // Some API DocIDs with types contain "{" and "}" to enclose the typeparam, which causes - // an exception to be thrown when trying to embed the string in a formatted string. - public static string Escaped(this string str) => str.Replace("{", "{{").Replace("}", "}}"); - } - -} diff --git a/DocsPortingTool/Log.cs b/DocsPortingTool/Log.cs deleted file mode 100644 index 3c29b9a..0000000 --- a/DocsPortingTool/Log.cs +++ /dev/null @@ -1,377 +0,0 @@ -using System; - -namespace DocsPortingTool -{ - public class Log - { - private static void WriteLine(string format, params object[]? args) - { - if (args == null || args.Length == 0) - { - Console.WriteLine(format); - } - else - { - Console.WriteLine(format, args); - } - } - - private static void Write(string format, params object[]? args) - { - if (args == null || args.Length == 0) - { - Console.Write(format); - } - else - { - Console.Write(format, args); - } - } - - public static void Print(bool endline, ConsoleColor foregroundColor, string format, params object[]? args) - { - ConsoleColor initialColor = Console.ForegroundColor; - Console.ForegroundColor = foregroundColor; - if (endline) - { - WriteLine(format, args); - } - else - { - Write(format, args); - } - Console.ForegroundColor = initialColor; - } - - public static void Info(string format) - { - Info(format, null); - } - - public static void Info(string format, params object[]? args) - { - Info(true, format, args); - } - - public static void Info(bool endline, string format, params object[]? args) - { - Print(endline, ConsoleColor.White, format, args); - } - - public static void Success(string format) - { - Success(format, null); - } - - public static void Success(string format, params object[]? args) - { - Success(true, format, args); - } - - public static void Success(bool endline, string format, params object[]? args) - { - Print(endline, ConsoleColor.Green, format, args); - } - - public static void Warning(string format) - { - Warning(format, null); - } - - public static void Warning(string format, params object[]? args) - { - Warning(true, format, args); - } - - public static void Warning(bool endline, string format, params object[]? args) - { - Print(endline, ConsoleColor.Yellow, format, args); - } - - public static void Error(string format) - { - Error(format, null); - } - - public static void Error(string format, params object[]? args) - { - Error(true, format, args); - } - - public static void Error(bool endline, string format, params object[]? args) - { - Print(endline, ConsoleColor.Red, format, args); - } - - public static void Cyan(string format) - { - Cyan(format, null); - } - - public static void Cyan(string format, params object[]? args) - { - Cyan(true, format, args); - } - - public static void Magenta(bool endline, string format, params object[]? args) - { - Print(endline, ConsoleColor.Magenta, format, args); - } - - public static void Magenta(string format) - { - Magenta(format, null); - } - - public static void Magenta(string format, params object[]? args) - { - Magenta(true, format, args); - } - - public static void Cyan(bool endline, string format, params object[]? args) - { - Print(endline, ConsoleColor.Cyan, format, args); - } - - public static void Assert(bool condition, string format, params object[]? args) - { - Assert(true, condition, format, args); - } - - public static void Assert(bool endline, bool condition, string format, params object[]? args) - { - if (condition) - { - Success(endline, format, args); - } - else - { - Error(endline, format, args); - } - } - - public static void Line() - { - Console.WriteLine(); - } - - public delegate void PrintHelpFunction(); - - public static void LogErrorAndExit(string format, params object[]? args) - { - Error(format, args); - Environment.Exit(0); - } - - public static void LogErrorPrintHelpAndExit(string format, params object[]? args) - { - Error(format, args); - PrintHelp(); - Environment.Exit(0); - } - - public static void PrintHelp() - { - Cyan(@" -This tool finds and ports triple slash comments found in .NET repos but do not yet exist in the dotnet-api-docs repo. - -The instructions below assume %SourceRepos% is the root folder of all your git cloned projects. - -Options: - - - MANDATORY - ------------------------------------------------------------ - | PARAMETER | TYPE | DESCRIPTION | - ------------------------------------------------------------ - - -Docs folder path The absolute directory root path where the Docs xml files are located. - Known locations: - > Runtime: %SourceRepos%\dotnet-api-docs\xml - > WPF: %SourceRepos%\dotnet-api-docs\xml - > WinForms: %SourceRepos%\dotnet-api-docs\xml - > ASP.NET MVC: %SourceRepos%\AspNetApiDocs\aspnet-mvc\xml - > ASP.NET Core: %SourceRepos%\AspNetApiDocs\aspnet-core\xml - Usage example: - -Docs %SourceRepos%\dotnet-api-docs\xml,%SourceRepos%\AspNetApiDocs\aspnet-mvc\xml - - -TripleSlash comma-separated A comma separated list (no spaces) of absolute directory paths where we should recursively - folder paths look for triple slash comment xml files. - Known locations: - > Runtime: %SourceRepos%\runtime\artifacts\bin\ - > CoreCLR: %SourceRepos%\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release\IL\ - > WinForms: %SourceRepos%\winforms\artifacts\bin\ - > WPF: %SourceRepos%\wpf\.tools\native\bin\dotnet-api-docs_netcoreapp3.0\0.0.0.1\_intellisense\netcore-3.0\ - Usage example: - -TripleSlash %SourceRepos%\corefx\artifacts\bin\,%SourceRepos%\winforms\artifacts\bin\ - - -IncludedAssemblies string list Comma separated list (no spaces) of assemblies to include. - Usage example: - -IncludedAssemblies System.IO,System.Runtime - - IMPORTANT: - Namespaces usually match the assembly name. There are some exceptions, like with types that live in - the System.Runtime assembly. For those cases, make sure to also specify the -IncludedNamespaces argument. - - - OPTIONAL - ------------------------------------------------------------ - | PARAMETER | TYPE | DESCRIPTION | - ------------------------------------------------------------ - - -h | -Help no arguments Displays this help message. If used, nothing else is processed and the program exits. - - -DisablePrompts bool Default is false (prompts are disabled). - Avoids prompting the user for input to correct some particular errors. - Usage example: - -DisablePrompts true - - -ExceptionCollisionThreshold int (0-100) Default is 70 (If >=70% of words collide, the string is not ported). - Decides how sensitive the detection of existing exception strings should be. - The tool compares the Docs exception string with the Triple Slash exception string. - If the number of words found in the Docs exception is below the specified threshold, - then the Triple Slash string is appended at the end of the Docs string. - The user is expected to verify the value. - The reason for this is that exceptions go through language review, and may contain more - than one root cause (separated by '-or-'), and there is no easy way to know if the string - has already been ported or not. - Usage example: - -ExceptionCollisionThreshold 60 - - -ExcludedAssemblies string list Default is empty (does not ignore any assemblies/namespaces). - Comma separated list (no spaces) of specific .NET assemblies/namespaces to ignore. - Usage example: - -ExcludedAssemblies System.IO.Compression,System.IO.Pipes - - -ExcludedNamespaces string list Default is empty (does not exclude any namespaces from the specified assemblies). - Comma separated list (no spaces) of specific namespaces to exclude from the specified assemblies. - Usage example: - -ExcludedNamespaces System.Runtime.Intrinsics,System.Reflection.Metadata - - -ExcludedTypes string list Default is empty (does not ignore any types). - Comma separated list (no spaces) of names of types to ignore. - Usage example: - -ExcludedTypes ArgumentException,Stream - - -IncludedNamespaces string list Default is empty (includes all namespaces from the specified assemblies). - Comma separated list (no spaces) of specific namespaces to include from the specified assemblies. - Usage example: - -IncludedNamespaces System,System.Data - - -IncludedTypes string list Default is empty (includes all types in the desired assemblies/namespaces). - Comma separated list (no spaces) of specific types to include. - Usage example: - -IncludedTypes FileStream,DirectoryInfo - - -PortExceptionsExisting bool Default is false (does not find and append existing exceptions). - Enable or disable finding, porting and appending summaries from existing exceptions. - Setting this to true can result in a lot of noise because there is - no easy way to detect if an exception summary has been ported already or not, - especially after it went through language review. - See `-ExceptionCollisionThreshold` to set the collision sensitivity. - Usage example: - -PortExceptionsExisting true - - -PortExceptionsNew bool Default is true (ports new exceptions). - Enable or disable finding and porting new exceptions. - Usage example: - -PortExceptionsNew false - - -PortMemberParams bool Default is true (ports Member parameters). - Enable or disable finding and porting Member parameters. - Usage example: - -PortMemberParams false - - -PortMemberProperties bool Default is true (ports Member properties). - Enable or disable finding and porting Member properties. - Usage example: - -PortMemberProperties false - - -PortMemberReturns bool Default is true (ports Member return values). - Enable or disable finding and porting Member return values. - Usage example: - -PortMemberReturns false - - -PortMemberRemarks bool Default is true (ports Member remarks). - Enable or disable finding and porting Member remarks. - Usage example: - -PortMemberRemarks false - - -PortMemberSummaries bool Default is true (ports Member summaries). - Enable or disable finding and porting Member summaries. - Usage example: - -PortMemberSummaries false - - -PortMemberTypeParams bool Default is true (ports Member TypeParams). - Enable or disable finding and porting Member TypeParams. - Usage example: - -PortMemberTypeParams false - - -PortTypeParams bool Default is true (ports Type Params). - Enable or disable finding and porting Type Params. - Usage example: - -PortTypeParams false - - -PortTypeRemarks bool Default is true (ports Type remarks). - Enable or disable finding and porting Type remarks. - Usage example: - -PortTypeRemarks false - - -PortTypeSummaries bool Default is true (ports Type summaries). - Enable or disable finding and porting Type summaries. - Usage example: - -PortTypeSummaries false - - -PortTypeTypeParams bool Default is true (ports Type TypeParams). - Enable or disable finding and porting Type TypeParams. - Usage example: - -PortTypeTypeParams false - - -PrintUndoc bool Default is false (prints a basic summary). - Prints a detailed summary of all the docs APIs that are undocumented. - Usage example: - -PrintUndoc true - - -Save bool Default is false (does not save changes). - Whether you want to save the changes in the dotnet-api-docs xml files. - Usage example: - -Save true - - -SkipInterfaceImplementations bool Default is false (includes interface implementations). - Whether you want the original interface documentation to be considered to fill the - undocumented API's documentation when the API itself does not provide its own documentation. - Setting this to false will include Explicit Interface Implementations as well. - Usage example: - -SkipInterfaceImplementations true - - -SkipInterfaceRemarks bool Default is true (excludes appending interface remarks). - Whether you want interface implementation remarks to be used when the API itself has no remarks. - Very noisy and generally the content in those remarks do not apply to the API that implements - the interface API. - Usage example: - -SkipInterfaceRemarks false - - "); - Warning(@" - tl;dr: Just specify these parameters: - - -Docs - -TripleSlash [,,...,] - -IncludedAssemblies [,,...] - -Save true - - Example: - DocsPortingTool \ - -Docs D:\dotnet-api-docs\xml \ - -TripleSlash D:\runtime\artifacts\bin \ - -IncludedAssemblies System.IO.FileSystem,System.Runtime.Intrinsics \ - -Save true -"); - Magenta(@" - Note: - If the names of your assemblies differ from the namespaces wheres your APIs live, specify the -IncludedNamespaces argument too. - - "); - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Properties/launchSettings.json b/DocsPortingTool/Properties/launchSettings.json deleted file mode 100644 index da4e252..0000000 --- a/DocsPortingTool/Properties/launchSettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "profiles": { - "DocsPortingTool": { - "commandName": "Project", - "commandLineArgs": "-TripleSlash D:\\runtime\\artifacts\\bin,D:\\runtime\\artifacts\\bin\\coreclr\\Windows_NT.x64.Release\\IL -Docs D:\\dotnet-api-docs\\xml -Save false -SkipInterfaceImplementations true -IncludedAssemblies System.Private.CoreLib -IncludedNamespaces System.Threading.Tasks -IncludedTypes Tasks", - "environmentVariables": { - "DOCS_IOT": "D:\\iot\\artifacts\\bin", - "DOCS_CORECLR": "D:\\runtime\\artifacts\\bin\\coreclr\\Windows_NT.x64.Release\\IL\\", - "DOCS_WINFORMS": "D:\\winforms\\artifacts\\bin\\", - "DOCS_WPF": "D:\\wpf\\.tools\\native\\bin\\dotnet-api-docs_netcoreapp3.0\\0.0.0.1\\_intellisense\\\\netcore-3.0\\", - "DOCS_RUNTIME": "D:\\runtime\\artifacts\\bin\\" - } - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/TripleSlash/TripleSlashCommentsContainer.cs b/DocsPortingTool/TripleSlash/TripleSlashCommentsContainer.cs deleted file mode 100644 index 8223a48..0000000 --- a/DocsPortingTool/TripleSlash/TripleSlashCommentsContainer.cs +++ /dev/null @@ -1,186 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Xml.Linq; - -/* -The triple slash comments xml files for... -A) corefx are saved in: - corefx/artifacts/bin/ -B) coreclr are saved in: - coreclr\packages\microsoft.netcore.app\\ref\netcoreapp\ - or in: - corefx/artifacts/bin/docs - but in this case, only namespaces found in coreclr/src/System.Private.CoreLib/shared need to be searched here. - -Each xml file represents a namespace. -The files are structured like this: - -root - assembly (1) - name (1) - members (many) - member(0:M) - summary (0:1) - param (0:M) - returns (0:1) - exception (0:M) - Note: The exception value may contain xml nodes. -*/ -namespace DocsPortingTool.TripleSlash -{ - public class TripleSlashCommentsContainer - { - private Configuration Config { get; set; } - - private XDocument? xDoc = null; - - public List Members = new List(); - - public int TotalFiles - { - get - { - return Members.Count; - } - } - - public TripleSlashCommentsContainer(Configuration config) - { - Config = config; - } - - public void CollectFiles() - { - Log.Info("Looking for triple slash xml files..."); - - foreach (FileInfo fileInfo in EnumerateFiles()) - { - LoadFile(fileInfo, printSuccess: true); - } - - Log.Success("Finished looking for triple slash xml files."); - Log.Line(); - } - - private IEnumerable EnumerateFiles() - { - foreach (DirectoryInfo dirInfo in Config.DirsTripleSlashXmls) - { - // 1) Find all the xml files inside all the subdirectories inside the triple slash xml directory - foreach (DirectoryInfo subDir in dirInfo.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)) - { - if (!Configuration.ForbiddenDirectories.Contains(subDir.Name) && !subDir.Name.EndsWith(".Tests")) - { - foreach (FileInfo fileInfo in subDir.EnumerateFiles("*.xml", SearchOption.AllDirectories)) - { - yield return fileInfo; - } - } - } - - // 2) Find all the xml files in the top directory - foreach (FileInfo fileInfo in dirInfo.EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly)) - { - yield return fileInfo; - } - } - } - - private void LoadFile(FileInfo fileInfo, bool printSuccess) - { - if (!fileInfo.Exists) - { - Log.Error($"Triple slash xml file does not exist: {fileInfo.FullName}"); - return; - } - - xDoc = XDocument.Load(fileInfo.FullName); - - if (IsXmlMalformed(xDoc, fileInfo.FullName, out string? assembly)) - { - return; - } - - int totalAdded = 0; - if (XmlHelper.TryGetChildElement(xDoc.Root, "members", out XElement? xeMembers) && xeMembers != null) - { - foreach (XElement xeMember in xeMembers.Elements("member")) - { - TripleSlashMember member = new TripleSlashMember(xeMember, assembly); - - if (Config.IncludedAssemblies.Any(included => member.Assembly.StartsWith(included)) && - !Config.ExcludedAssemblies.Any(excluded => member.Assembly.StartsWith(excluded))) - { - // No namespaces provided by the user means they want to port everything from that assembly - if (!Config.IncludedNamespaces.Any() || - (Config.IncludedNamespaces.Any(included => member.Namespace.StartsWith(included)) && - !Config.ExcludedNamespaces.Any(excluded => member.Namespace.StartsWith(excluded)))) - { - totalAdded++; - Members.Add(member); - } - } - } - } - - if (printSuccess && totalAdded > 0) - { - Log.Success($"{totalAdded} triple slash member(s) added from xml file '{fileInfo.FullName}'"); - } - } - - private bool IsXmlMalformed(XDocument xDoc, string fileName, [NotNullWhen(returnValue: false)] out string? assembly) - { - assembly = null; - - if (xDoc.Root == null) - { - Log.Error($"Triple slash xml file does not contain a root element: {fileName}"); - return true; - } - - if (xDoc.Root.Name != "doc") - { - Log.Error($"Triple slash xml file does not contain a doc element: {fileName}"); - return true; - } - - if (!xDoc.Root.HasElements) - { - Log.Error($"Triple slash xml file doc element not have any children: {fileName}"); - return true; - } - - if (xDoc.Root.Elements("assembly").Count() != 1) - { - Log.Error($"Triple slash xml file does not contain exactly 1 'assembly' element: {fileName}"); - return true; - } - - if (xDoc.Root.Elements("members").Count() != 1) - { - Log.Error($"Triple slash xml file does not contain exactly 1 'members' element: {fileName}"); - return true; - } - - XElement xAssembly = xDoc.Root.Element("assembly"); - if (xAssembly.Elements("name").Count() != 1) - { - Log.Error($"Triple slash xml file assembly element does not contain exactly 1 'name' element: {fileName}"); - return true; - } - - assembly = xAssembly.Element("name").Value; - if (string.IsNullOrEmpty(assembly)) - { - Log.Error($"Triple slash xml file assembly string is null or empty: {fileName}"); - return true; - } - - return false; - } - } -} diff --git a/DocsPortingTool/TripleSlash/TripleSlashException.cs b/DocsPortingTool/TripleSlash/TripleSlashException.cs deleted file mode 100644 index 0adba3e..0000000 --- a/DocsPortingTool/TripleSlash/TripleSlashException.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.TripleSlash -{ - public class TripleSlashException - { - public XElement XEException - { - get; - private set; - } - - private string _cref = string.Empty; - public string Cref - { - get - { - if (string.IsNullOrWhiteSpace(_cref)) - { - _cref = XmlHelper.GetAttributeValue(XEException, "cref"); - } - return _cref; - } - } - - private string _value = string.Empty; - public string Value - { - get - { - if (string.IsNullOrWhiteSpace(_value)) - { - _value = XmlHelper.GetNodesInPlainText(XEException); - } - return _value; - } - } - - public TripleSlashException(XElement xeException) - { - XEException = xeException; - } - - public override string ToString() - { - return $"{Cref} - {Value}"; - } - } -} diff --git a/DocsPortingTool/TripleSlash/TripleSlashMember.cs b/DocsPortingTool/TripleSlash/TripleSlashMember.cs deleted file mode 100644 index 1036af0..0000000 --- a/DocsPortingTool/TripleSlash/TripleSlashMember.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace DocsPortingTool.TripleSlash -{ - public class TripleSlashMember - { - private readonly XElement XEMember; - - public string Assembly { get; private set; } - - - private string _namespace = string.Empty; - public string Namespace - { - get - { - if (string.IsNullOrWhiteSpace(_namespace)) - { - string[] splittedParenthesis = Name.Split('(', StringSplitOptions.RemoveEmptyEntries); - string withoutParenthesisAndPrefix = splittedParenthesis[0][2..]; // Exclude the "X:" prefix - string[] splittedDots = withoutParenthesisAndPrefix.Split('.', StringSplitOptions.RemoveEmptyEntries); - - _namespace = string.Join('.', splittedDots.Take(splittedDots.Length - 1)); - } - - return _namespace; - } - } - - private string? _name; - /// - /// The API DocId. - /// - public string Name - { - get - { - if (_name == null) - { - _name = XmlHelper.GetAttributeValue(XEMember, "name"); - } - return _name; - } - } - - private List? _params; - public List Params - { - get - { - if (_params == null) - { - _params = XEMember.Elements("param").Select(x => new TripleSlashParam(x)).ToList(); - } - return _params; - } - } - - private List? _typeParams; - public List TypeParams - { - get - { - if (_typeParams == null) - { - _typeParams = XEMember.Elements("typeparam").Select(x => new TripleSlashTypeParam(x)).ToList(); - } - return _typeParams; - } - } - - private List? _exceptions; - public IEnumerable Exceptions - { - get - { - if (_exceptions == null) - { - _exceptions = XEMember.Elements("exception").Select(x => new TripleSlashException(x)).ToList(); - } - return _exceptions; - } - } - - private string? _summary; - public string Summary - { - get - { - if (_summary == null) - { - XElement xElement = XEMember.Element("summary"); - _summary = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; - } - return _summary; - } - } - - public string? _value; - public string Value - { - get - { - if (_value == null) - { - XElement xElement = XEMember.Element("value"); - _value = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; - } - return _value; - } - } - - private string? _returns; - public string Returns - { - get - { - if (_returns == null) - { - XElement xElement = XEMember.Element("returns"); - _returns = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; - } - return _returns; - } - } - - private string? _remarks; - public string Remarks - { - get - { - if (_remarks == null) - { - XElement xElement = XEMember.Element("remarks"); - _remarks = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; - } - return _remarks; - } - } - - public TripleSlashMember(XElement xeMember, string assembly) - { - if (string.IsNullOrEmpty(assembly)) - { - throw new ArgumentNullException(nameof(assembly)); - } - - XEMember = xeMember ?? throw new ArgumentNullException(nameof(xeMember)); - Assembly = assembly; - } - - public override string ToString() - { - return Name; - } - } -} diff --git a/DocsPortingTool/TripleSlash/TripleSlashParam.cs b/DocsPortingTool/TripleSlash/TripleSlashParam.cs deleted file mode 100644 index f8ef49f..0000000 --- a/DocsPortingTool/TripleSlash/TripleSlashParam.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.TripleSlash -{ - public class TripleSlashParam - { - public XElement XEParam - { - get; - private set; - } - - private string _name = string.Empty; - public string Name - { - get - { - if (string.IsNullOrWhiteSpace(_name)) - { - _name = XmlHelper.GetAttributeValue(XEParam, "name"); - } - return _name; - } - } - - private string _value = string.Empty; - public string Value - { - get - { - if (string.IsNullOrWhiteSpace(_value)) - { - _value = XmlHelper.GetNodesInPlainText(XEParam); - } - return _value; - } - } - - public TripleSlashParam(XElement xeParam) - { - XEParam = xeParam; - } - } -} diff --git a/DocsPortingTool/TripleSlash/TripleSlashTypeParam.cs b/DocsPortingTool/TripleSlash/TripleSlashTypeParam.cs deleted file mode 100644 index 510cc95..0000000 --- a/DocsPortingTool/TripleSlash/TripleSlashTypeParam.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.TripleSlash -{ - public class TripleSlashTypeParam - { - public XElement XETypeParam; - - private string _name = string.Empty; - public string Name - { - get - { - if (string.IsNullOrWhiteSpace(_name)) - { - _name = XmlHelper.GetAttributeValue(XETypeParam, "name"); - } - return _name; - } - } - - private string _value = string.Empty; - public string Value - { - get - { - if (string.IsNullOrWhiteSpace(_value)) - { - _value = XmlHelper.GetNodesInPlainText(XETypeParam); - } - return _value; - } - } - - public TripleSlashTypeParam(XElement xeTypeParam) - { - XETypeParam = xeTypeParam; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/XmlHelper.cs b/DocsPortingTool/XmlHelper.cs deleted file mode 100644 index 4b6effc..0000000 --- a/DocsPortingTool/XmlHelper.cs +++ /dev/null @@ -1,319 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using System.Xml; -using System.Xml.Linq; - -namespace DocsPortingTool -{ - public 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" }, - }; - - public static string GetAttributeValue(XElement parent, string name) - { - if (parent == null) - { - Log.Error("A null parent was passed when attempting to get attribute '{0}'", name); - throw new ArgumentNullException(nameof(parent)); - } - else - { - XAttribute attr = parent.Attribute(name); - if (attr != null) - { - return attr.Value.Trim(); - } - } - return string.Empty; - } - - public static bool TryGetChildElement(XElement parent, string name, out XElement? child) - { - child = null; - - if (parent == null || string.IsNullOrWhiteSpace(name)) - return false; - - child = parent.Element(name); - - return child != null; - } - - public static string GetChildElementValue(XElement parent, string childName) - { - XElement child = parent.Element(childName); - - if (child != null) - { - return GetNodesInPlainText(child); - } - - return string.Empty; - } - - public static string GetNodesInPlainText(XElement element) - { - if (element == null) - { - Log.Error("A null element was passed when attempting to retrieve the nodes in plain text."); - throw new ArgumentNullException(nameof(element)); - } - return string.Join("", element.Nodes()).Trim(); - } - - public static void SaveFormattedAsMarkdown(XElement element, string newValue, bool isMember) - { - if (element == null) - { - Log.Error("A null element was passed when attempting to save formatted as markdown"); - throw new ArgumentNullException(nameof(element)); - } - - // 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 = RemoveUndesiredEndlines(newValue); - updatedValue = SubstituteRemarksRegexPatterns(updatedValue); - updatedValue = ReplaceMarkdownPatterns(updatedValue); - - 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) - { - Log.Error("A null parent was passed when attempting to add child formatted as markdown"); - throw new ArgumentNullException(nameof(parent)); - } - - if (child == null) - { - Log.Error("A null child was passed when attempting to add child formatted as markdown"); - throw new ArgumentNullException(nameof(child)); - } - - SaveFormattedAsMarkdown(child, childValue, isMember); - parent.Add(child); - } - - public static void SaveFormattedAsXml(XElement element, string newValue, bool removeUndesiredEndlines = true) - { - if (element == null) - { - Log.Error("A null element was passed when attempting to save formatted as xml"); - throw new ArgumentNullException(nameof(element)); - } - - 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) - { - Log.Error("A null element was passed when attempting to append formatted as xml"); - throw new ArgumentNullException(nameof(element)); - } - - SaveFormattedAsXml(element, GetNodesInPlainText(element) + valueToAppend, removeUndesiredEndlines); - } - - public static void AddChildFormattedAsXml(XElement parent, XElement child, string childValue) - { - if (parent == null) - { - Log.Error("A null parent was passed when attempting to add child formatted as xml"); - throw new ArgumentNullException(nameof(parent)); - } - - if (child == null) - { - Log.Error("A null child was passed when attempting to add child formatted as xml"); - throw new ArgumentNullException(nameof(child)); - } - - SaveFormattedAsXml(child, childValue); - parent.Add(child); - } - - private static string RemoveUndesiredEndlines(string value) - { - Regex regex = new Regex(@"((?'undesiredEndlinePrefix'[^\.\:])(\r\n)+[ \t]*)"); - string newValue = value; - if (regex.IsMatch(value)) - { - newValue = regex.Replace(value, @"${undesiredEndlinePrefix} "); - } - return newValue.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 (updatedValue.Contains(kvp.Key)) - { - updatedValue = updatedValue.Replace(kvp.Key, kvp.Value); - } - } - 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); - } - } - 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; - } - - private static string SubstituteRegexPatterns(string value, Dictionary replaceableRegexPatterns) - { - foreach (KeyValuePair pattern in replaceableRegexPatterns) - { - Regex regex = new Regex(pattern.Key); - if (regex.IsMatch(value)) - { - value = regex.Replace(value, pattern.Value); - } - } - - return value; - } - } -} \ No newline at end of file From eb5c1c6dab6c4d61fb1f6bdff4445446483d4219 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:41:58 -0800 Subject: [PATCH 02/65] Docs changes. --- Libraries/Docs/APIKind.cs | 8 + Libraries/Docs/DocsAPI.cs | 261 ++++++++++++++++++++ Libraries/Docs/DocsAssemblyInfo.cs | 38 +++ Libraries/Docs/DocsAttribute.cs | 29 +++ Libraries/Docs/DocsCommentsContainer.cs | 308 ++++++++++++++++++++++++ Libraries/Docs/DocsException.cs | 104 ++++++++ Libraries/Docs/DocsMember.cs | 224 +++++++++++++++++ Libraries/Docs/DocsMemberSignature.cs | 30 +++ Libraries/Docs/DocsParam.cs | 41 ++++ Libraries/Docs/DocsParameter.cs | 27 +++ Libraries/Docs/DocsSeeAlso.cs | 34 +++ Libraries/Docs/DocsType.cs | 199 +++++++++++++++ Libraries/Docs/DocsTypeParam.cs | 44 ++++ Libraries/Docs/DocsTypeParameter.cs | 64 +++++ Libraries/Docs/DocsTypeSignature.cs | 30 +++ Libraries/Docs/IDocsAPI.cs | 22 ++ 16 files changed, 1463 insertions(+) create mode 100644 Libraries/Docs/APIKind.cs create mode 100644 Libraries/Docs/DocsAPI.cs create mode 100644 Libraries/Docs/DocsAssemblyInfo.cs create mode 100644 Libraries/Docs/DocsAttribute.cs create mode 100644 Libraries/Docs/DocsCommentsContainer.cs create mode 100644 Libraries/Docs/DocsException.cs create mode 100644 Libraries/Docs/DocsMember.cs create mode 100644 Libraries/Docs/DocsMemberSignature.cs create mode 100644 Libraries/Docs/DocsParam.cs create mode 100644 Libraries/Docs/DocsParameter.cs create mode 100644 Libraries/Docs/DocsSeeAlso.cs create mode 100644 Libraries/Docs/DocsType.cs create mode 100644 Libraries/Docs/DocsTypeParam.cs create mode 100644 Libraries/Docs/DocsTypeParameter.cs create mode 100644 Libraries/Docs/DocsTypeSignature.cs create mode 100644 Libraries/Docs/IDocsAPI.cs diff --git a/Libraries/Docs/APIKind.cs b/Libraries/Docs/APIKind.cs new file mode 100644 index 0000000..00f554e --- /dev/null +++ b/Libraries/Docs/APIKind.cs @@ -0,0 +1,8 @@ +namespace Libraries.Docs +{ + internal enum APIKind + { + Type, + Member + } +} diff --git a/Libraries/Docs/DocsAPI.cs b/Libraries/Docs/DocsAPI.cs new file mode 100644 index 0000000..86eead0 --- /dev/null +++ b/Libraries/Docs/DocsAPI.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal abstract class DocsAPI : IDocsAPI + { + private string? _docIdEscaped = null; + private List? _params; + private List? _parameters; + private List? _typeParameters; + private List? _typeParams; + private List? _assemblyInfos; + private List? _seeAlsos; + + protected readonly XElement XERoot; + + protected DocsAPI(XElement xeRoot) => XERoot = xeRoot; + + public abstract bool Changed { get; set; } + public string FilePath { get; set; } = string.Empty; + public abstract string DocId { get; } + + /// + /// The Parameter elements found inside the Parameters section. + /// + public List Parameters + { + get + { + if (_parameters == null) + { + XElement? xeParameters = XERoot.Element("Parameters"); + if (xeParameters == null) + { + _parameters = new(); + } + else + { + _parameters = xeParameters.Elements("Parameter").Select(x => new DocsParameter(x)).ToList(); + } + } + return _parameters; + } + } + + /// + /// The TypeParameter elements found inside the TypeParameters section. + /// + public List TypeParameters + { + get + { + if (_typeParameters == null) + { + XElement? xeTypeParameters = XERoot.Element("TypeParameters"); + if (xeTypeParameters == null) + { + _typeParameters = new(); + } + else + { + _typeParameters = xeTypeParameters.Elements("TypeParameter").Select(x => new DocsTypeParameter(x)).ToList(); + } + } + return _typeParameters; + } + } + + public XElement Docs + { + get + { + return XERoot.Element("Docs"); + } + } + + /// + /// 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; + } + } + + /// + /// 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 SeeAlsos + { + get + { + if (_seeAlsos == null) + { + if (Docs != null) + { + _seeAlsos = Docs.Elements("seealso").Select(x => new DocsSeeAlso(this, x)).ToList(); + } + else + { + _seeAlsos = new(); + } + } + return _seeAlsos; + } + } + + public abstract string Summary { get; set; } + + public abstract string Remarks { get; set; } + + public List AssemblyInfos + { + get + { + if (_assemblyInfos == null) + { + _assemblyInfos = new List(); + } + return _assemblyInfos; + } + } + + public string DocIdEscaped + { + get + { + if (_docIdEscaped == null) + { + _docIdEscaped = DocId.Replace("<", "{").Replace(">", "}").Replace("<", "{").Replace(">", "}"); + } + return _docIdEscaped; + } + } + + 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 APIKind Kind + { + get + { + return this switch + { + DocsMember _ => APIKind.Member, + DocsType _ => APIKind.Type, + _ => throw new ArgumentException("Unrecognized IDocsAPI object") + }; + } + } + + public DocsTypeParam AddTypeParam(string name, string value) + { + XElement typeParam = new XElement("typeparam"); + typeParam.SetAttributeValue("name", name); + XmlHelper.AddChildFormattedAsXml(Docs, typeParam, value); + Changed = true; + return new DocsTypeParam(this, typeParam); + } + + 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; + } + } + + // 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) + { + element = null; + + if (Docs == null) + { + return false; + } + + element = Docs.Element(name); + + if (element == null && addIfMissing) + { + element = new XElement(name); + XmlHelper.AddChildFormattedAsXml(Docs, element, Configuration.ToBeAdded); + } + + return element != null; + } + } +} diff --git a/Libraries/Docs/DocsAssemblyInfo.cs b/Libraries/Docs/DocsAssemblyInfo.cs new file mode 100644 index 0000000..7840315 --- /dev/null +++ b/Libraries/Docs/DocsAssemblyInfo.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsAssemblyInfo + { + private readonly XElement XEAssemblyInfo; + public string AssemblyName + { + get + { + return 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 DocsAssemblyInfo(XElement xeAssemblyInfo) + { + XEAssemblyInfo = xeAssemblyInfo; + } + + public override string ToString() => AssemblyName; + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsAttribute.cs b/Libraries/Docs/DocsAttribute.cs new file mode 100644 index 0000000..a07ad42 --- /dev/null +++ b/Libraries/Docs/DocsAttribute.cs @@ -0,0 +1,29 @@ +using System.Xml.Linq; + +namespace Libraries.Docs +{ + 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 DocsAttribute(XElement xeAttribute) + { + XEAttribute = xeAttribute; + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsCommentsContainer.cs b/Libraries/Docs/DocsCommentsContainer.cs new file mode 100644 index 0000000..0d9fc44 --- /dev/null +++ b/Libraries/Docs/DocsCommentsContainer.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsCommentsContainer + { + private Configuration Config { get; set; } + + private XDocument? xDoc = null; + + public readonly List Types = new List(); + public readonly List Members = new List(); + + public DocsCommentsContainer(Configuration config) + { + Config = config; + } + + public void CollectFiles() + { + Log.Info("Looking for Docs xml files..."); + + foreach (FileInfo fileInfo in EnumerateFiles()) + { + LoadFile(fileInfo); + } + + Log.Success("Finished looking for Docs xml files."); + Log.Line(); + } + + public void Save() + { + if (!Config.Save) + { + Log.Line(); + Log.Error("[No files were saved]"); + Log.Warning($"Did you forget to specify '-{nameof(Config.Save)} true'?"); + Log.Line(); + + return; + } + + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + Encoding encoding = Encoding.GetEncoding(1252); // Preserves original xml encoding from Docs repo + + List savedFiles = new List(); + foreach (var type in Types.Where(x => x.Changed)) + { + Log.Warning(false, $"Saving changes for {type.FilePath}:"); + + try + { + StreamReader sr = new StreamReader(type.FilePath); + int x = sr.Read(); // Force the first read to be done so the encoding is detected + sr.Close(); + + // These settings prevent the addition of the element on the first line and will preserve indentation+endlines + XmlWriterSettings xws = new XmlWriterSettings + { + OmitXmlDeclaration = true, + Indent = true, + Encoding = encoding, + CheckCharacters = false + }; + + using (XmlWriter xw = XmlWriter.Create(type.FilePath, xws)) + { + type.XDoc.Save(xw); + } + + // Workaround to delete the annoying endline added by XmlWriter.Save + string fileData = File.ReadAllText(type.FilePath); + if (!fileData.EndsWith(Environment.NewLine)) + { + File.WriteAllText(type.FilePath, fileData + Environment.NewLine, encoding); + } + + Log.Success(" [Saved]"); + } + catch (Exception e) + { + Log.Error(e.Message); + Log.Line(); + Log.Error(e.StackTrace ?? string.Empty); + if (e.InnerException != null) + { + Log.Line(); + Log.Error(e.InnerException.Message); + Log.Line(); + Log.Error(e.InnerException.StackTrace ?? string.Empty); + } + System.Threading.Thread.Sleep(1000); + } + + Log.Line(); + } + } + + private bool HasAllowedDirName(DirectoryInfo dirInfo) + { + return !Configuration.ForbiddenBinSubdirectories.Contains(dirInfo.Name) && !dirInfo.Name.EndsWith(".Tests"); + } + + private bool HasAllowedFileName(FileInfo fileInfo) + { + return !fileInfo.Name.StartsWith("ns-") && + fileInfo.Name != "index.xml" && + fileInfo.Name != "_filter.xml"; + } + + private IEnumerable EnumerateFiles() + { + var includedAssembliesAndNamespaces = Config.IncludedAssemblies.Concat(Config.IncludedNamespaces); + var excludedAssembliesAndNamespaces = Config.ExcludedAssemblies.Concat(Config.ExcludedNamespaces); + + foreach (DirectoryInfo rootDir in Config.DirsDocsXml) + { + // Try to find folders with the names of assemblies AND namespaces (if the user specified any) + foreach (string included in includedAssembliesAndNamespaces) + { + // If the user specified a sub-assembly or sub-namespace to exclude, we need to skip it + if (excludedAssembliesAndNamespaces.Any(excluded => included.StartsWith(excluded))) + { + continue; + } + + foreach (DirectoryInfo subDir in rootDir.EnumerateDirectories($"{included}*", SearchOption.TopDirectoryOnly)) + { + if (HasAllowedDirName(subDir)) + { + foreach (FileInfo fileInfo in subDir.EnumerateFiles("*.xml", SearchOption.AllDirectories)) + { + if (HasAllowedFileName(fileInfo)) + { + // LoadFile will determine if the Type is allowed or not + yield return fileInfo; + } + } + } + } + + if (!Config.SkipInterfaceImplementations) + { + // Find interfaces only inside System.* folders. + // Including Microsoft.* folders reaches the max limit of files to include in a list, plus there are no essential interfaces there. + foreach (DirectoryInfo subDir in rootDir.EnumerateDirectories("System*", SearchOption.AllDirectories)) + { + if (!Configuration.ForbiddenBinSubdirectories.Contains(subDir.Name) && + // Exclude any folder that starts with the excluded assemblies OR excluded namespaces + !excludedAssembliesAndNamespaces.Any(excluded => subDir.Name.StartsWith(excluded)) && !subDir.Name.EndsWith(".Tests")) + { + // Ensure including interface files that start with I and then an uppercase letter, and prevent including files like 'Int' + foreach (FileInfo fileInfo in subDir.EnumerateFiles("I*.xml", SearchOption.AllDirectories)) + { + if (fileInfo.Name[1] >= 'A' || fileInfo.Name[1] <= 'Z') + { + yield return fileInfo; + } + } + } + } + } + } + } + } + + private void LoadFile(FileInfo fileInfo) + { + if (!fileInfo.Exists) + { + Log.Error($"Docs xml file does not exist: {fileInfo.FullName}"); + return; + } + + xDoc = XDocument.Load(fileInfo.FullName); + + if (IsXmlMalformed(xDoc, fileInfo.FullName)) + { + return; + } + + DocsType docsType = new DocsType(fileInfo.FullName, xDoc, xDoc.Root!); + + bool add = false; + bool addedAsInterface = false; + + bool containsForbiddenAssembly = docsType.AssemblyInfos.Any(assemblyInfo => + Config.ExcludedAssemblies.Any(excluded => assemblyInfo.AssemblyName.StartsWith(excluded)) || + Config.ExcludedNamespaces.Any(excluded => assemblyInfo.AssemblyName.StartsWith(excluded))); + + if (!containsForbiddenAssembly) + { + // If it's an interface, always add it if the user wants to detect EIIs, + // even if it's in an assembly that was not included but was not explicitly excluded + addedAsInterface = false; + if (!Config.SkipInterfaceImplementations) + { + // Interface files start with I, and have an 2nd alphabetic character + addedAsInterface = docsType.Name.Length >= 2 && docsType.Name[0] == 'I' && docsType.Name[1] >= 'A' && docsType.Name[1] <= 'Z'; + add |= addedAsInterface; + + } + + bool containsAllowedAssembly = docsType.AssemblyInfos.Any(assemblyInfo => + Config.IncludedAssemblies.Any(included => assemblyInfo.AssemblyName.StartsWith(included)) || + Config.IncludedNamespaces.Any(included => assemblyInfo.AssemblyName.StartsWith(included))); + + if (containsAllowedAssembly) + { + // If it was already added above as an interface, skip this part + // Otherwise, find out if the type belongs to the included assemblies, and if specified, to the included (and not excluded) types + // This includes interfaces even if user wants to skip EIIs - They will be added if they belong to this namespace or to the list of + // included (and not exluded) types, but will not be used for EII, but rather as normal types whose comments should be ported + if (!addedAsInterface) + { + // Either the user didn't specify namespace filtering (allow all namespaces) or specified particular ones to include/exclude + if (!Config.IncludedNamespaces.Any() || + (Config.IncludedNamespaces.Any(included => docsType.Namespace.StartsWith(included)) && + !Config.ExcludedNamespaces.Any(excluded => docsType.Namespace.StartsWith(excluded)))) + { + // Can add if the user didn't specify type filtering (allow all types), or specified particular ones to include/exclude + add = !Config.IncludedTypes.Any() || + (Config.IncludedTypes.Contains(docsType.Name) && + !Config.ExcludedTypes.Contains(docsType.Name)); + } + } + } + } + + if (add) + { + int totalMembersAdded = 0; + Types.Add(docsType); + + if (XmlHelper.TryGetChildElement(xDoc.Root!, "Members", out XElement? xeMembers) && xeMembers != null) + { + foreach (XElement xeMember in xeMembers.Elements("Member")) + { + DocsMember member = new DocsMember(fileInfo.FullName, docsType, xeMember); + totalMembersAdded++; + Members.Add(member); + } + } + + string message = $"Type {docsType.DocId} added with {totalMembersAdded} member(s) included."; + if (addedAsInterface) + { + Log.Magenta("[Interface] - " + message); + } + else if (totalMembersAdded == 0) + { + Log.Warning(message); + } + else + { + Log.Success(message); + } + } + } + + private bool IsXmlMalformed(XDocument? xDoc, string fileName) + { + if(xDoc == null) + { + Log.Error($"XDocument is null: {fileName}"); + return true; + } + if (xDoc.Root == null) + { + Log.Error($"Docs xml file does not have a root element: {fileName}"); + return true; + } + + if (xDoc.Root.Name == "Namespace") + { + Log.Error($"Skipping namespace file (should have been filtered already): {fileName}"); + return true; + } + + if (xDoc.Root.Name != "Type") + { + Log.Error($"Docs xml file does not have a 'Type' root element: {fileName}"); + return true; + } + + if (!xDoc.Root.HasElements) + { + Log.Error($"Docs xml file Type element does not have any children: {fileName}"); + return true; + } + + if (xDoc.Root.Elements("Docs").Count() != 1) + { + Log.Error($"Docs xml file Type element does not have a Docs child: {fileName}"); + return true; + } + + return false; + } + } +} diff --git a/Libraries/Docs/DocsException.cs b/Libraries/Docs/DocsException.cs new file mode 100644 index 0000000..1450887 --- /dev/null +++ b/Libraries/Docs/DocsException.cs @@ -0,0 +1,104 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsException + { + private readonly XElement XEException; + + public IDocsAPI ParentAPI + { + get; private set; + } + + public string Cref + { + get + { + return XmlHelper.GetAttributeValue(XEException, "cref"); + } + } + + public string Value + { + get + { + return XmlHelper.GetNodesInPlainText(XEException); + } + private set + { + XmlHelper.SaveFormattedAsXml(XEException, value); + } + } + + public string OriginalValue { get; private set; } + + public DocsException(IDocsAPI parentAPI, XElement xException) + { + ParentAPI = parentAPI; + XEException = 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; + } + } +} diff --git a/Libraries/Docs/DocsMember.cs b/Libraries/Docs/DocsMember.cs new file mode 100644 index 0000000..1383004 --- /dev/null +++ b/Libraries/Docs/DocsMember.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsMember : DocsAPI + { + private string? _memberName; + private List? _memberSignatures; + private string? _docId; + private List? _altMemberCref; + private List? _exceptions; + + public DocsMember(string filePath, DocsType parentType, XElement xeMember) + : base(xeMember) + { + FilePath = filePath; + ParentType = parentType; + AssemblyInfos.AddRange(XERoot.Elements("AssemblyInfo").Select(x => new DocsAssemblyInfo(x))); + } + + public DocsType ParentType { get; private set; } + + public override bool Changed + { + get => ParentType.Changed; + set => ParentType.Changed |= value; + } + + 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 override string DocId + { + get + { + if (_docId == null) + { + _docId = string.Empty; + DocsMemberSignature? ms = MemberSignatures.FirstOrDefault(x => x.Language == "DocId"); + if (ms == null) + { + string message = string.Format("Could not find a DocId MemberSignature for '{0}'", MemberName); + Log.Error(message); + throw new MissingMemberException(message); + } + _docId = ms.Value; + } + return _docId; + } + } + + public string MemberType + { + get + { + return XmlHelper.GetChildElementValue(XERoot, "MemberType"); + } + } + + public string ImplementsInterfaceMember + { + get + { + XElement xeImplements = XERoot.Element("Implements"); + if (xeImplements != null) + { + return XmlHelper.GetChildElementValue(xeImplements, "InterfaceMember"); + } + return string.Empty; + } + } + + public string ReturnType + { + get + { + XElement xeReturnValue = XERoot.Element("ReturnValue"); + if (xeReturnValue != null) + { + return XmlHelper.GetChildElementValue(xeReturnValue, "ReturnType"); + } + return string.Empty; + } + } + + public 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: {DocIdEscaped}"); + } + } + } + + public override string Summary + { + get + { + return GetNodesInPlainText("summary"); + } + set + { + SaveFormattedAsXml("summary", value, addIfMissing: true); + } + } + + public override string Remarks + { + get + { + return GetNodesInPlainText("remarks"); + } + set + { + SaveFormattedAsMarkdown("remarks", value, addIfMissing: !value.IsDocsEmpty(), isMember: true); + } + } + + 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: {DocIdEscaped}"); + } + } + } + + public List AltMemberCref + { + get + { + if (_altMemberCref == null) + { + if (Docs != null) + { + _altMemberCref = Docs.Elements("altmember").Select(x => XmlHelper.GetAttributeValue(x, "cref")).ToList(); + } + else + { + _altMemberCref = new List(); + } + } + return _altMemberCref; + } + } + + public List Exceptions + { + get + { + if (_exceptions == null) + { + if (Docs != null) + { + _exceptions = Docs.Elements("exception").Select(x => new DocsException(this, x)).ToList(); + } + else + { + _exceptions = new List(); + } + } + return _exceptions; + } + } + + public override string ToString() + { + return DocId; + } + + public DocsException AddException(string cref, string value) + { + XElement exception = new XElement("exception"); + exception.SetAttributeValue("cref", cref); + XmlHelper.AddChildFormattedAsXml(Docs, exception, value); + Changed = true; + return new DocsException(this, exception); + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsMemberSignature.cs b/Libraries/Docs/DocsMemberSignature.cs new file mode 100644 index 0000000..f9eff57 --- /dev/null +++ b/Libraries/Docs/DocsMemberSignature.cs @@ -0,0 +1,30 @@ +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsMemberSignature + { + private readonly XElement XEMemberSignature; + + public string Language + { + get + { + return XmlHelper.GetAttributeValue(XEMemberSignature, "Language"); + } + } + + public string Value + { + get + { + return XmlHelper.GetAttributeValue(XEMemberSignature, "Value"); + } + } + + public DocsMemberSignature(XElement xeMemberSignature) + { + XEMemberSignature = xeMemberSignature; + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsParam.cs b/Libraries/Docs/DocsParam.cs new file mode 100644 index 0000000..c5a09b2 --- /dev/null +++ b/Libraries/Docs/DocsParam.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; + +namespace Libraries.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 DocsParam(IDocsAPI parentAPI, XElement xeDocsParam) + { + ParentAPI = parentAPI; + XEDocsParam = xeDocsParam; + } + public override string ToString() + { + return Name; + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsParameter.cs b/Libraries/Docs/DocsParameter.cs new file mode 100644 index 0000000..ec598b8 --- /dev/null +++ b/Libraries/Docs/DocsParameter.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; + +namespace Libraries.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; + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsSeeAlso.cs b/Libraries/Docs/DocsSeeAlso.cs new file mode 100644 index 0000000..ca218b8 --- /dev/null +++ b/Libraries/Docs/DocsSeeAlso.cs @@ -0,0 +1,34 @@ +#nullable enable +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsSeeAlso + { + private readonly XElement XESeeAlso; + + public IDocsAPI ParentAPI + { + get; private set; + } + + public string Cref + { + get + { + return XmlHelper.GetAttributeValue(XESeeAlso, "cref"); + } + } + + public DocsSeeAlso(IDocsAPI parentAPI, XElement xeSeeAlso) + { + ParentAPI = parentAPI; + XESeeAlso = xeSeeAlso; + } + + public override string ToString() + { + return $"{Cref}"; + } + } +} diff --git a/Libraries/Docs/DocsType.cs b/Libraries/Docs/DocsType.cs new file mode 100644 index 0000000..6731f87 --- /dev/null +++ b/Libraries/Docs/DocsType.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + /// + /// Represents the root xml element (unique) of a Docs xml file, called Type. + /// + internal class DocsType : DocsAPI + { + private string? _name; + private string? _fullName; + private string? _namespace; + private string? _docId; + private string? _baseTypeName; + private List? _interfaceNames; + private List? _attributes; + private List? _typesSignatures; + + public DocsType(string filePath, XDocument xDoc, XElement xeRoot) + : base(xeRoot) + { + FilePath = filePath; + XDoc = xDoc; + AssemblyInfos.AddRange(XERoot.Elements("AssemblyInfo").Select(x => new DocsAssemblyInfo(x))); + } + + public XDocument XDoc { get; set; } + + public override bool Changed { get; set; } + + public string Name + { + get + { + if (_name == null) + { + _name = XmlHelper.GetAttributeValue(XERoot, "Name"); + } + return _name; + } + } + + public string FullName + { + get + { + if (_fullName == null) + { + _fullName = XmlHelper.GetAttributeValue(XERoot, "FullName"); + } + return _fullName; + } + } + + public string Namespace + { + get + { + if (_namespace == null) + { + int lastDotPosition = FullName.LastIndexOf('.'); + _namespace = lastDotPosition < 0 ? FullName : FullName.Substring(0, lastDotPosition); + } + return _namespace; + } + } + + public List TypeSignatures + { + get + { + if (_typesSignatures == null) + { + _typesSignatures = XERoot.Elements("TypeSignature").Select(x => new DocsTypeSignature(x)).ToList(); + } + return _typesSignatures; + } + } + + public override string DocId + { + get + { + if (_docId == null) + { + DocsTypeSignature? dts = TypeSignatures.FirstOrDefault(x => x.Language == "DocId"); + if (dts == null) + { + string message = $"DocId TypeSignature not found for FullName"; + Log.Error($"DocId TypeSignature not found for FullName"); + throw new MissingMemberException(message); + } + _docId = dts.Value; + } + return _docId; + } + } + + public XElement? Base + { + get + { + return XERoot.Element("Base"); + } + } + + public string BaseTypeName + { + get + { + if (Base == null) + { + _baseTypeName = string.Empty; + } + else if (_baseTypeName == null) + { + _baseTypeName = XmlHelper.GetChildElementValue(Base, "BaseTypeName"); + } + return _baseTypeName; + } + } + + public XElement? Interfaces + { + get + { + return XERoot.Element("Interfaces"); + } + } + + public List InterfaceNames + { + get + { + if (Interfaces == null) + { + _interfaceNames = new(); + } + else if (_interfaceNames == null) + { + _interfaceNames = Interfaces.Elements("Interface").Select(x => XmlHelper.GetChildElementValue(x, "InterfaceName")).ToList(); + } + return _interfaceNames; + } + } + + public List Attributes + { + get + { + if (_attributes == null) + { + XElement? e = XERoot.Element("Attributes"); + if (e == null) + { + _attributes = new(); + } + else + { + _attributes = (e != null) ? e.Elements("Attribute").Select(x => new DocsAttribute(x)).ToList() : new List(); + } + } + return _attributes; + } + } + + public override string Summary + { + get + { + return GetNodesInPlainText("summary"); + } + set + { + SaveFormattedAsXml("summary", value, addIfMissing: true); + } + } + + public override string Remarks + { + get + { + return GetNodesInPlainText("remarks"); + } + set + { + SaveFormattedAsMarkdown("remarks", value, addIfMissing: !value.IsDocsEmpty(), isMember: false); + } + } + + public override string ToString() + { + return FullName; + } + } +} diff --git a/Libraries/Docs/DocsTypeParam.cs b/Libraries/Docs/DocsTypeParam.cs new file mode 100644 index 0000000..af2ea7a --- /dev/null +++ b/Libraries/Docs/DocsTypeParam.cs @@ -0,0 +1,44 @@ +#nullable enable +using System.Xml.Linq; + +namespace Libraries.Docs +{ + /// + /// Each one of these typeparam objects live inside the Docs section inside the Member object. + /// + internal class DocsTypeParam + { + private readonly XElement XEDocsTypeParam; + public IDocsAPI ParentAPI + { + get; private set; + } + + public string Name + { + get + { + return XmlHelper.GetAttributeValue(XEDocsTypeParam, "name"); + } + } + + public string Value + { + get + { + return XmlHelper.GetNodesInPlainText(XEDocsTypeParam); + } + set + { + XmlHelper.SaveFormattedAsXml(XEDocsTypeParam, value); + ParentAPI.Changed = true; + } + } + + public DocsTypeParam(IDocsAPI parentAPI, XElement xeDocsTypeParam) + { + ParentAPI = parentAPI; + XEDocsTypeParam = xeDocsTypeParam; + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsTypeParameter.cs b/Libraries/Docs/DocsTypeParameter.cs new file mode 100644 index 0000000..ac66251 --- /dev/null +++ b/Libraries/Docs/DocsTypeParameter.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + /// + /// Each one of these TypeParameter objects islocated inside the TypeParameters section inside the Member. + /// + internal class DocsTypeParameter + { + private readonly XElement XETypeParameter; + public string Name + { + get + { + return XmlHelper.GetAttributeValue(XETypeParameter, "Name"); + } + } + private XElement? Constraints + { + get + { + return XETypeParameter.Element("Constraints"); + } + } + private List? _constraintsParamterAttributes; + public List ConstraintsParameterAttributes + { + get + { + if (_constraintsParamterAttributes == null) + { + if (Constraints != null) + { + _constraintsParamterAttributes = Constraints.Elements("ParameterAttribute").Select(x => XmlHelper.GetNodesInPlainText(x)).ToList(); + } + else + { + _constraintsParamterAttributes = new List(); + } + } + return _constraintsParamterAttributes; + } + } + + public string ConstraintsBaseTypeName + { + get + { + if (Constraints != null) + { + return XmlHelper.GetChildElementValue(Constraints, "BaseTypeName"); + } + return string.Empty; + } + } + + public DocsTypeParameter(XElement xeTypeParameter) + { + XETypeParameter = xeTypeParameter; + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsTypeSignature.cs b/Libraries/Docs/DocsTypeSignature.cs new file mode 100644 index 0000000..5ca5c46 --- /dev/null +++ b/Libraries/Docs/DocsTypeSignature.cs @@ -0,0 +1,30 @@ +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsTypeSignature + { + private readonly XElement XETypeSignature; + + public string Language + { + get + { + return XmlHelper.GetAttributeValue(XETypeSignature, "Language"); + } + } + + public string Value + { + get + { + return XmlHelper.GetAttributeValue(XETypeSignature, "Value"); + } + } + + public DocsTypeSignature(XElement xeTypeSignature) + { + XETypeSignature = xeTypeSignature; + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/IDocsAPI.cs b/Libraries/Docs/IDocsAPI.cs new file mode 100644 index 0000000..837e371 --- /dev/null +++ b/Libraries/Docs/IDocsAPI.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal interface IDocsAPI + { + public abstract APIKind Kind { get; } + public abstract bool Changed { get; set; } + public abstract string FilePath { get; set; } + public abstract string DocId { get; } + public abstract XElement Docs { get; } + public abstract List Parameters { get; } + public abstract List Params { get; } + public abstract List TypeParameters { get; } + public abstract List TypeParams { get; } + public abstract string Summary { get; set; } + public abstract string Remarks { get; set; } + public abstract DocsParam SaveParam(XElement xeCoreFXParam); + public abstract DocsTypeParam AddTypeParam(string name, string value); + } +} From 318a78ab2c1a68e2736f4cf2ede431ba441a1abe Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:42:16 -0800 Subject: [PATCH 03/65] IntelliSense XML rename and changes. --- .../IntelliSenseXmlCommentsContainer.cs | 190 ++++++++++++++++++ .../IntelliSenseXmlException.cs | 49 +++++ .../IntelliSenseXml/IntelliSenseXmlMember.cs | 160 +++++++++++++++ .../IntelliSenseXml/IntelliSenseXmlParam.cs | 44 ++++ .../IntelliSenseXmlTypeParam.cs | 40 ++++ 5 files changed, 483 insertions(+) create mode 100644 Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs create mode 100644 Libraries/IntelliSenseXml/IntelliSenseXmlException.cs create mode 100644 Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs create mode 100644 Libraries/IntelliSenseXml/IntelliSenseXmlParam.cs create mode 100644 Libraries/IntelliSenseXml/IntelliSenseXmlTypeParam.cs diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs new file mode 100644 index 0000000..9a167ba --- /dev/null +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs @@ -0,0 +1,190 @@ +#nullable enable +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +/* +The IntelliSense xml comments files for... +A) corefx are saved in: + corefx/artifacts/bin/ +B) coreclr are saved in: + coreclr\packages\microsoft.netcore.app\\ref\netcoreapp\ + or in: + corefx/artifacts/bin/docs + but in this case, only namespaces found in coreclr/src/System.Private.CoreLib/shared need to be searched here. + +Each xml file represents a namespace. +The files are structured like this: + +root + assembly (1) + name (1) + members (many) + member(0:M) + summary (0:1) + param (0:M) + returns (0:1) + exception (0:M) + Note: The exception value may contain xml nodes. +*/ +namespace Libraries.IntelliSenseXml +{ + internal class IntelliSenseXmlCommentsContainer + { + private Configuration Config { get; set; } + + private XDocument? xDoc = null; + + // The IntelliSense xml files do not separate types from members, like ECMA xml files do - Everything is a member. + public List Members = new List(); + + public IntelliSenseXmlCommentsContainer(Configuration config) + { + Config = config; + } + + public void CollectFiles() + { + Log.Info("Looking for IntelliSense xml files..."); + + foreach (FileInfo fileInfo in EnumerateFiles()) + { + LoadFile(fileInfo, printSuccess: true); + } + + Log.Success("Finished looking for IntelliSense xml files."); + Log.Line(); + } + + private IEnumerable EnumerateFiles() + { + foreach (DirectoryInfo dirInfo in Config.DirsIntelliSense) + { + // 1) Find all the xml files inside all the subdirectories inside the IntelliSense xml directory + foreach (DirectoryInfo subDir in dirInfo.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)) + { + if (!Configuration.ForbiddenBinSubdirectories.Contains(subDir.Name) && !subDir.Name.EndsWith(".Tests")) + { + foreach (FileInfo fileInfo in subDir.EnumerateFiles("*.xml", SearchOption.AllDirectories)) + { + yield return fileInfo; + } + } + } + + // 2) Find all the xml files in the top directory + foreach (FileInfo fileInfo in dirInfo.EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly)) + { + yield return fileInfo; + } + } + } + + private void LoadFile(FileInfo fileInfo, bool printSuccess) + { + if (!fileInfo.Exists) + { + Log.Error($"The IntelliSense xml file does not exist: {fileInfo.FullName}"); + return; + } + + xDoc = XDocument.Load(fileInfo.FullName); + + if (TryGetAssemblyName(xDoc, fileInfo.FullName, out string? assembly)) + { + return; + } + + int totalAdded = 0; + if (XmlHelper.TryGetChildElement(xDoc.Root!, "members", out XElement? xeMembers) && xeMembers != null) + { + foreach (XElement xeMember in xeMembers.Elements("member")) + { + IntelliSenseXmlMember member = new IntelliSenseXmlMember(xeMember, assembly); + + if (Config.IncludedAssemblies.Any(included => member.Assembly.StartsWith(included)) && + !Config.ExcludedAssemblies.Any(excluded => member.Assembly.StartsWith(excluded))) + { + // No namespaces provided by the user means they want to port everything from that assembly + if (!Config.IncludedNamespaces.Any() || + (Config.IncludedNamespaces.Any(included => member.Namespace.StartsWith(included)) && + !Config.ExcludedNamespaces.Any(excluded => member.Namespace.StartsWith(excluded)))) + { + totalAdded++; + Members.Add(member); + } + } + } + } + + if (printSuccess && totalAdded > 0) + { + Log.Success($"{totalAdded} IntelliSense xml member(s) added from xml file '{fileInfo.FullName}'"); + } + } + + // Verifies the file is properly formed while attempting to retrieve the assembly name. + private bool TryGetAssemblyName(XDocument? xDoc, string fileName, [NotNullWhen(returnValue: false)] out string? assembly) + { + assembly = null; + + if (xDoc == null) + { + Log.Error($"The XDocument was null: {fileName}"); + return true; + } + if (xDoc.Root == null) + { + Log.Error($"The IntelliSense xml file does not contain a root element: {fileName}"); + return true; + } + + if (xDoc.Root.Name != "doc") + { + Log.Error($"The IntelliSense xml file does not contain a doc element: {fileName}"); + return true; + } + + if (!xDoc.Root.HasElements) + { + Log.Error($"The IntelliSense xml file doc element not have any children: {fileName}"); + return true; + } + + if (xDoc.Root.Elements("assembly").Count() != 1) + { + Log.Error($"The IntelliSense xml file does not contain exactly 1 'assembly' element: {fileName}"); + return true; + } + + if (xDoc.Root.Elements("members").Count() != 1) + { + Log.Error($"The IntelliSense xml file does not contain exactly 1 'members' element: {fileName}"); + return true; + } + + XElement? xAssembly = xDoc.Root.Element("assembly"); + if (xAssembly == null) + { + Log.Error($"The assembly xElement is null: {fileName}"); + return true; + } + if (xAssembly.Elements("name").Count() != 1) + { + Log.Error($"The IntelliSense xml file assembly element does not contain exactly 1 'name' element: {fileName}"); + return true; + } + + assembly = xAssembly.Element("name")!.Value; + if (string.IsNullOrEmpty(assembly)) + { + Log.Error($"The IntelliSense xml file assembly string is null or empty: {fileName}"); + return true; + } + + return false; + } + } +} diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlException.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlException.cs new file mode 100644 index 0000000..44b3645 --- /dev/null +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlException.cs @@ -0,0 +1,49 @@ +using System.Xml.Linq; + +namespace Libraries.IntelliSenseXml +{ + internal class IntelliSenseXmlException + { + public XElement XEException + { + get; + private set; + } + + private string _cref = string.Empty; + public string Cref + { + get + { + if (string.IsNullOrWhiteSpace(_cref)) + { + _cref = XmlHelper.GetAttributeValue(XEException, "cref"); + } + return _cref; + } + } + + private string _value = string.Empty; + public string Value + { + get + { + if (string.IsNullOrWhiteSpace(_value)) + { + _value = XmlHelper.GetNodesInPlainText(XEException); + } + return _value; + } + } + + public IntelliSenseXmlException(XElement xeException) + { + XEException = xeException; + } + + public override string ToString() + { + return $"{Cref} - {Value}"; + } + } +} diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs new file mode 100644 index 0000000..c0c56e8 --- /dev/null +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Libraries.IntelliSenseXml +{ + internal class IntelliSenseXmlMember + { + private readonly XElement XEMember; + + public string Assembly { get; private set; } + + + private string _namespace = string.Empty; + public string Namespace + { + get + { + if (string.IsNullOrWhiteSpace(_namespace)) + { + string[] splittedParenthesis = Name.Split('(', StringSplitOptions.RemoveEmptyEntries); + string withoutParenthesisAndPrefix = splittedParenthesis[0][2..]; // Exclude the "X:" prefix + string[] splittedDots = withoutParenthesisAndPrefix.Split('.', StringSplitOptions.RemoveEmptyEntries); + + _namespace = string.Join('.', splittedDots.Take(splittedDots.Length - 1)); + } + + return _namespace; + } + } + + private string? _name; + /// + /// The API DocId. + /// + public string Name + { + get + { + if (_name == null) + { + _name = XmlHelper.GetAttributeValue(XEMember, "name"); + } + return _name; + } + } + + private List? _params; + public List Params + { + get + { + if (_params == null) + { + _params = XEMember.Elements("param").Select(x => new IntelliSenseXmlParam(x)).ToList(); + } + return _params; + } + } + + private List? _typeParams; + public List TypeParams + { + get + { + if (_typeParams == null) + { + _typeParams = XEMember.Elements("typeparam").Select(x => new IntelliSenseXmlTypeParam(x)).ToList(); + } + return _typeParams; + } + } + + private List? _exceptions; + public IEnumerable Exceptions + { + get + { + if (_exceptions == null) + { + _exceptions = XEMember.Elements("exception").Select(x => new IntelliSenseXmlException(x)).ToList(); + } + return _exceptions; + } + } + + private string? _summary; + public string Summary + { + get + { + if (_summary == null) + { + XElement xElement = XEMember.Element("summary"); + _summary = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; + } + return _summary; + } + } + + public string? _value; + public string Value + { + get + { + if (_value == null) + { + XElement xElement = XEMember.Element("value"); + _value = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; + } + return _value; + } + } + + private string? _returns; + public string Returns + { + get + { + if (_returns == null) + { + XElement xElement = XEMember.Element("returns"); + _returns = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; + } + return _returns; + } + } + + private string? _remarks; + public string Remarks + { + get + { + if (_remarks == null) + { + XElement xElement = XEMember.Element("remarks"); + _remarks = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; + } + return _remarks; + } + } + + public IntelliSenseXmlMember(XElement xeMember, string assembly) + { + if (string.IsNullOrEmpty(assembly)) + { + throw new ArgumentNullException(nameof(assembly)); + } + + XEMember = xeMember ?? throw new ArgumentNullException(nameof(xeMember)); + Assembly = assembly; + } + + public override string ToString() + { + return Name; + } + } +} diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlParam.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlParam.cs new file mode 100644 index 0000000..b5931a1 --- /dev/null +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlParam.cs @@ -0,0 +1,44 @@ +using System.Xml.Linq; + +namespace Libraries.IntelliSenseXml +{ + internal class IntelliSenseXmlParam + { + public XElement XEParam + { + get; + private set; + } + + private string _name = string.Empty; + public string Name + { + get + { + if (string.IsNullOrWhiteSpace(_name)) + { + _name = XmlHelper.GetAttributeValue(XEParam, "name"); + } + return _name; + } + } + + private string _value = string.Empty; + public string Value + { + get + { + if (string.IsNullOrWhiteSpace(_value)) + { + _value = XmlHelper.GetNodesInPlainText(XEParam); + } + return _value; + } + } + + public IntelliSenseXmlParam(XElement xeParam) + { + XEParam = xeParam; + } + } +} diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlTypeParam.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlTypeParam.cs new file mode 100644 index 0000000..7fda8f2 --- /dev/null +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlTypeParam.cs @@ -0,0 +1,40 @@ +using System.Xml.Linq; + +namespace Libraries.IntelliSenseXml +{ + internal class IntelliSenseXmlTypeParam + { + public XElement XETypeParam; + + private string _name = string.Empty; + public string Name + { + get + { + if (string.IsNullOrWhiteSpace(_name)) + { + _name = XmlHelper.GetAttributeValue(XETypeParam, "name"); + } + return _name; + } + } + + private string _value = string.Empty; + public string Value + { + get + { + if (string.IsNullOrWhiteSpace(_value)) + { + _value = XmlHelper.GetNodesInPlainText(XETypeParam); + } + return _value; + } + } + + public IntelliSenseXmlTypeParam(XElement xeTypeParam) + { + XETypeParam = xeTypeParam; + } + } +} \ No newline at end of file From 5a0447600124b70ad284f3c677144acefc0619c4 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:42:28 -0800 Subject: [PATCH 04/65] Triple Slash rewriter. --- .../TripleSlashSyntaxRewriter.cs | 409 ++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs new file mode 100644 index 0000000..b86008d --- /dev/null +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -0,0 +1,409 @@ +#nullable enable +using Libraries.Docs; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Libraries.RoslynTripleSlash +{ + internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter + { + private const string BoilerplateText = "Comments located in main file."; + + private DocsCommentsContainer DocsComments { get; } + private SemanticModel Model { get; } + private bool UseBoilerplate { get; } + + public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticModel model, Location location, SyntaxTree tree, bool useBoilerplate) : base(visitIntoStructuredTrivia: true) + { + DocsComments = docsComments; + Model = model; + UseBoilerplate = useBoilerplate; + } + + public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) + { + SyntaxNode? baseNode = base.VisitClassDeclaration(node); + + ISymbol? symbol = Model.GetDeclaredSymbol(node); + if (symbol == null) + { + Log.Warning($"Symbol is null."); + return baseNode; + } + + return VisitType(baseNode, symbol); + } + + public override SyntaxNode? VisitConstructorDeclaration(ConstructorDeclarationSyntax node) => + VisitBaseMethodDeclaration(node); + + public override SyntaxNode? VisitEnumDeclaration(EnumDeclarationSyntax node) => + VisitMemberDeclaration(node); + + public override SyntaxNode? VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) => + VisitMemberDeclaration(node); + + public override SyntaxNode? VisitEventDeclaration(EventDeclarationSyntax node) => + VisitMemberDeclaration(node); + + public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node) => + VisitMemberDeclaration(node); + + public override SyntaxNode? VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) + { + SyntaxNode? baseNode = base.VisitInterfaceDeclaration(node); + + ISymbol? symbol = Model.GetDeclaredSymbol(node); + if (symbol == null) + { + Log.Warning($"Symbol is null."); + return baseNode; + } + + return VisitType(baseNode, symbol); + } + + public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) => + VisitBaseMethodDeclaration(node); + + public override SyntaxNode? VisitPropertyDeclaration(PropertyDeclarationSyntax node) + { + if (!TryGetMember(node, out DocsMember? member)) + { + return node; + } + + string summaryText = BoilerplateText; + string valueText = BoilerplateText; + + if (!UseBoilerplate) + { + summaryText = member.Summary; + valueText = member.Value; + } + + SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); + + SyntaxTriviaList summary = GetSummary(summaryText, leadingWhitespace); + SyntaxTriviaList value = GetValue(valueText, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); + + return GetNodeWithTrivia(node, summary, value, remarks, exceptions, seealsos); + } + + public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node) + { + SyntaxNode? baseNode = base.VisitStructDeclaration(node); + + ISymbol? symbol = Model.GetDeclaredSymbol(node); + if (symbol == null) + { + Log.Warning($"Symbol is null."); + return baseNode; + } + + return VisitType(baseNode, symbol); + } + + private SyntaxNode? VisitType(SyntaxNode? node, 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; + } + + string summaryText = BoilerplateText; + string remarksText = string.Empty; + + if (!UseBoilerplate) + { + if (!TryGetType(node, symbol, out DocsType? type)) + { + return node; + } + + summaryText = type.Summary; + remarksText = type.Remarks; + } + + SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); + + SyntaxTriviaList summary = GetSummary(summaryText, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(remarksText, leadingWhitespace); + + return GetNodeWithTrivia(node, summary, remarks); + } + + private SyntaxNode GetNodeWithTrivia(SyntaxNode node, params SyntaxTriviaList[] trivias) + { + SyntaxTriviaList finalTrivia = new(SyntaxFactory.CarriageReturnLineFeed); // Space to separate from previous definition + foreach (SyntaxTriviaList t in trivias) + { + finalTrivia = finalTrivia.AddRange(t); + } + finalTrivia = finalTrivia.AddRange(GetLeadingWhitespace(node)); // spaces before type declaration + + return node.WithLeadingTrivia(finalTrivia); + } + + private SyntaxNode? VisitBaseMethodDeclaration(BaseMethodDeclarationSyntax node) + { + if (!TryGetMember(node, out DocsMember? member)) + { + return node; + } + + SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); + + SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); + + SyntaxTriviaList parameters = new(); + foreach (SyntaxTriviaList parameterTrivia in member.Params.Select( + param => GetParam(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) + { + parameters = parameters.AddRange(parameterTrivia); + } + + SyntaxTriviaList typeParameters = new(); + foreach (SyntaxTriviaList typeParameterTrivia in member.TypeParams.Select( + param => GetTypeParam(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) + { + typeParameters = typeParameters.AddRange(typeParameterTrivia); + } + + SyntaxTriviaList returns = GetReturns(UseBoilerplate ? BoilerplateText : member.Returns, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); + + return GetNodeWithTrivia(node, summary, parameters, typeParameters, returns, remarks, exceptions, seealsos); + } + + private SyntaxNode? VisitMemberDeclaration(MemberDeclarationSyntax node) + { + if (!TryGetMember(node, out DocsMember? member)) + { + return node; + } + + SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); + + SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + + SyntaxTriviaList exceptions = new(); + // No need to add exceptions in secondary files + if (!UseBoilerplate && member.Exceptions.Any()) + { + foreach (SyntaxTriviaList exceptionsTrivia in member.Exceptions.Select( + exception => GetException(exception.Cref, exception.Value, leadingWhitespace))) + { + exceptions = exceptions.AddRange(exceptionsTrivia); + } + } + + return GetNodeWithTrivia(node, summary, remarks, exceptions); + } + + private SyntaxTriviaList GetLeadingWhitespace(SyntaxNode node) => + node.GetLeadingTrivia().Where(t => t.IsKind(SyntaxKind.WhitespaceTrivia)).ToSyntaxTriviaList(); + + private SyntaxTriviaList GetSummary(string text, SyntaxTriviaList leadingWhitespace) + { + SyntaxList contents = GetContentsInRows(text); + XmlElementSyntax element = SyntaxFactory.XmlSummaryElement(contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitespace) + { + if (!UseBoilerplate && !text.IsDocsEmpty()) + { + string trimmedRemarks = text.RemoveSubstrings("").Trim(); + SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed)); + XmlNodeSyntax xmlRemarksContent = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); + XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(xmlRemarksContent); + + return GetXmlTrivia(xmlRemarks, leadingWhitespace); + + //DocumentationCommentTriviaSyntax triviaNode = SyntaxFactory.DocumentationComment(SyntaxKind.SingleLineDocumentationCommentTrivia, content); + //SyntaxTrivia docCommentTrivia = SyntaxFactory.Trivia(triviaNode); + + //return leadingWhitespace + //.Add(docCommentTrivia) + //.Add(SyntaxFactory.CarriageReturnLineFeed); + } + + return new(); + } + + private SyntaxTriviaList GetValue(string text, SyntaxTriviaList leadingWhitespace) + { + SyntaxList contents = GetContentsInRows(text); + XmlElementSyntax element = SyntaxFactory.XmlValueElement(contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + private SyntaxTriviaList GetParam(string name, string text, SyntaxTriviaList leadingWhitespace) + { + SyntaxList contents = GetContentsInRows(text); + XmlElementSyntax element = SyntaxFactory.XmlParamElement(name, contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + private SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace) + { + var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute("name", text)); + SyntaxList contents = GetContentsInRows(text); + return GetXmlTrivia("typeparam", attribute, contents, leadingWhitespace); + } + + private SyntaxTriviaList GetReturns(string text, SyntaxTriviaList leadingWhitespace) + { + // For when returns is empty because the method returns void + if (string.IsNullOrWhiteSpace(text)) + { + return new(); + } + + SyntaxList contents = GetContentsInRows(text); + XmlElementSyntax element = SyntaxFactory.XmlReturnsElement(contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + private SyntaxTriviaList GetExceptions(DocsMember member, SyntaxTriviaList leadingWhitespace) + { + SyntaxTriviaList exceptions = new(); + // No need to add exceptions in secondary files + if (!UseBoilerplate && member.Exceptions.Any()) + { + foreach (SyntaxTriviaList exceptionsTrivia in member.Exceptions.Select( + exception => GetException(exception.Cref, exception.Value, leadingWhitespace))) + { + exceptions = exceptions.AddRange(exceptionsTrivia); + } + } + return exceptions; + } + + private SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace) + { + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); + XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text, leadingWhitespace)); + XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + private SyntaxTriviaList GetSeeAlsos(DocsMember member, SyntaxTriviaList leadingWhitespace) + { + SyntaxTriviaList seealsos = new(); + // No need to add exceptions in secondary files + if (!UseBoilerplate && member.SeeAlsos.Any()) + { + foreach (SyntaxTriviaList seealsoTrivia in member.SeeAlsos.Select( + s => GetSeeAlso(s.Cref, leadingWhitespace))) + { + seealsos = seealsos.AddRange(seealsoTrivia); + } + } + return seealsos; + } + + private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace) + { + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); + XmlEmptyElementSyntax element = SyntaxFactory.XmlSeeAlsoElement(crefSyntax); + return GetXmlTrivia(element, leadingWhitespace); + } + + private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace) + { + string whitespace = leadingWhitespace.ToFullString().Replace(Environment.NewLine, ""); + var tokens = new List(); + foreach (string line in text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)) + { + tokens.Add(SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace)); // Needs to be textnewline, and below needs to be textliteral + tokens.Add(SyntaxFactory.XmlTextLiteral(SyntaxTriviaList.Create(SyntaxFactory.SyntaxTrivia(SyntaxKind.DocumentationCommentExteriorTrivia, string.Empty)), line, line, default)); + } + + return SyntaxFactory.TokenList(tokens); + } + + private SyntaxList GetContentsInRows(string text) + { + return new(SyntaxFactory.XmlText(text)); // TODO: Press enter! + } + + private SyntaxTriviaList GetXmlTrivia(XmlNodeSyntax node, SyntaxTriviaList leadingWhitespace) + { + DocumentationCommentTriviaSyntax docComment = SyntaxFactory.DocumentationComment(node); + SyntaxTrivia docCommentTrivia = SyntaxFactory.Trivia(docComment); + + return leadingWhitespace + .Add(docCommentTrivia) + .Add(SyntaxFactory.CarriageReturnLineFeed); + } + + // Generates a custom SyntaxTrivia object containing a triple slashed xml element with optional attributes. + // Looks like below (excluding square brackets): + // [ /// text] + private SyntaxTriviaList GetXmlTrivia(string name, SyntaxList attributes, SyntaxList 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, contents, end); + + return GetXmlTrivia(element, leadingWhitespace); + } + + private bool TryGetMember(SyntaxNode node, [NotNullWhen(returnValue: true)] out DocsMember? member) + { + member = null; + if (Model.GetDeclaredSymbol(node) is ISymbol symbol) + { + string? docId = symbol.GetDocumentationCommentId(); + if (!string.IsNullOrWhiteSpace(docId)) + { + member = DocsComments.Members.FirstOrDefault(m => m.DocId == docId); + } + } + return member != null; + } + + private bool TryGetType(SyntaxNode node, ISymbol symbol, [NotNullWhen(returnValue: true)] out DocsType? type) + { + type = null; + + string? docId = symbol.GetDocumentationCommentId(); + if (!string.IsNullOrWhiteSpace(docId)) + { + type = DocsComments.Types.FirstOrDefault(t => t.DocId == docId); + } + + return type != null; + } + } +} From a15ef72d8956a94b481cea80004831861d3ede46 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:43:20 -0800 Subject: [PATCH 05/65] Shared code changes. --- Libraries/Configuration.cs | 660 +++++++++++++++++++++++ Libraries/Extensions.cs | 42 ++ Libraries/Libraries.csproj | 26 + Libraries/Log.cs | 408 ++++++++++++++ Libraries/ToDocsPorter.cs | 896 +++++++++++++++++++++++++++++++ Libraries/ToTripleSlashPorter.cs | 133 +++++ Libraries/XmlHelper.cs | 320 +++++++++++ 7 files changed, 2485 insertions(+) create mode 100644 Libraries/Configuration.cs create mode 100644 Libraries/Extensions.cs create mode 100644 Libraries/Libraries.csproj create mode 100644 Libraries/Log.cs create mode 100644 Libraries/ToDocsPorter.cs create mode 100644 Libraries/ToTripleSlashPorter.cs create mode 100644 Libraries/XmlHelper.cs diff --git a/Libraries/Configuration.cs b/Libraries/Configuration.cs new file mode 100644 index 0000000..b39af89 --- /dev/null +++ b/Libraries/Configuration.cs @@ -0,0 +1,660 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; + +namespace Libraries +{ + public class Configuration + { + private static readonly char Separator = ','; + + public enum PortingDirection + { + ToDocs, + ToTripleSlash + } + + private enum Mode + { + BinLog, + CsProj, + DisablePrompts, + Direction, + Docs, + ExceptionCollisionThreshold, + ExcludedAssemblies, + ExcludedNamespaces, + ExcludedTypes, + IncludedAssemblies, + IncludedNamespaces, + IncludedTypes, + Initial, + IntelliSense, + PortExceptionsExisting, + PortExceptionsNew, + PortMemberParams, + PortMemberProperties, + PortMemberReturns, + PortMemberRemarks, + PortMemberSummaries, + PortMemberTypeParams, + PortTypeParams, // Params of a Type + PortTypeRemarks, + PortTypeSummaries, + PortTypeTypeParams, // TypeParams of a Type + PrintUndoc, + Save, + SkipInterfaceImplementations, + SkipInterfaceRemarks + } + + // The default boilerplate string for what dotnet-api-docs + // considers an empty (undocumented) API element. + public static readonly string ToBeAdded = "To be added."; + + public static readonly string[] ForbiddenBinSubdirectories = new[] { "binplacePackages", "docs", "mscorlib", "native", "netfx", "netstandard", "pkg", "Product", "ref", "runtime", "shimsTargetRuntime", "testhost", "tests", "winrt" }; + + public readonly string BinLogPath = "output.binlog"; + public bool BinLogger { get; private set; } = false; + public FileInfo? CsProj { get; private set; } + public PortingDirection Direction { get; private set; } = PortingDirection.ToDocs; + public List DirsIntelliSense { get; } = new List(); + public List DirsDocsXml { get; } = new List(); + public bool DisablePrompts { get; set; } = false; + public int ExceptionCollisionThreshold { get; set; } = 70; + public HashSet ExcludedAssemblies { get; } = new HashSet(); + public HashSet ExcludedNamespaces { get; } = new HashSet(); + public HashSet ExcludedTypes { get; } = new HashSet(); + public HashSet IncludedAssemblies { get; } = new HashSet(); + public HashSet IncludedNamespaces { get; } = new HashSet(); + public HashSet IncludedTypes { get; } = new HashSet(); + public bool PortExceptionsExisting { get; set; } = false; + public bool PortExceptionsNew { get; set; } = true; + public bool PortMemberParams { get; set; } = true; + public bool PortMemberProperties { get; set; } = true; + public bool PortMemberReturns { get; set; } = true; + public bool PortMemberRemarks { get; set; } = true; + public bool PortMemberSummaries { get; set; } = true; + public bool PortMemberTypeParams { get; set; } = true; + /// + /// Params of a Type. + /// + public bool PortTypeParams { get; set; } = true; + public bool PortTypeRemarks { get; set; } = true; + public bool PortTypeSummaries { get; set; } = true; + /// + /// TypeParams of a Type. + /// + public bool PortTypeTypeParams { get; set; } = true; + public bool PrintUndoc { get; set; } = false; + public bool Save { get; set; } = false; + public bool SkipInterfaceImplementations { get; set; } = false; + public bool SkipInterfaceRemarks { get; set; } = true; + + public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) + { + Mode mode = Mode.Initial; + + Log.Info("Verifying CLI arguments..."); + + if (args == null || args.Length == 0) + { + Log.ErrorPrintHelpAndExit("No arguments passed to the executable."); + } + + Configuration config = new Configuration(); + + foreach (string arg in args!) + { + switch (mode) + { + case Mode.BinLog: + { + config.BinLogger = ParseOrExit(arg, "Create a binlog"); + mode = Mode.Initial; + break; + } + + case Mode.CsProj: + { + if (string.IsNullOrWhiteSpace(arg)) + { + Log.ErrorAndExit("You must specify a *.csproj path."); + } + else if (!File.Exists(arg)) + { + Log.ErrorAndExit($"The *.csproj file does not exist: {arg}"); + } + else + { + string ext = Path.GetExtension(arg).ToUpperInvariant(); + if (ext != ".CSPROJ") + { + Log.ErrorAndExit($"The file does not have a *.csproj extension: {arg}"); + } + } + config.CsProj = new FileInfo(arg); + mode = Mode.Initial; + break; + } + + case Mode.DisablePrompts: + { + config.DisablePrompts = ParseOrExit(arg, "Disable prompts"); + mode = Mode.Initial; + break; + } + + case Mode.Direction: + { + switch (arg.ToUpperInvariant()) + { + case "TODOCS": + config.Direction = PortingDirection.ToDocs; + break; + case "TOTRIPLESLASH": + config.Direction = PortingDirection.ToTripleSlash; + break; + default: + Log.ErrorAndExit($"Unrecognized direction value: {arg}"); + break; + } + mode = Mode.Initial; + break; + } + + case Mode.Docs: + { + string[] splittedDirPaths = arg.Split(',', StringSplitOptions.RemoveEmptyEntries); + + Log.Cyan($"Specified Docs xml locations:"); + foreach (string dirPath in splittedDirPaths) + { + DirectoryInfo dirInfo = new DirectoryInfo(dirPath); + if (!dirInfo.Exists) + { + Log.ErrorAndExit($"This Docs xml directory does not exist: {dirPath}"); + } + + config.DirsDocsXml.Add(dirInfo); + Log.Info($" - {dirPath}"); + } + + mode = Mode.Initial; + break; + } + + case Mode.ExceptionCollisionThreshold: + { + if (!int.TryParse(arg, out int value)) + { + Log.ErrorAndExit($"Invalid int value for 'Exception collision threshold' argument: {arg}"); + } + else if (value < 1 || value > 100) + { + Log.ErrorAndExit($"Value needs to be between 0 and 100: {value}"); + } + + config.ExceptionCollisionThreshold = value; + + Log.Cyan($"Exception collision threshold:"); + Log.Info($" - {value}"); + break; + } + + case Mode.ExcludedAssemblies: + { + string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + + if (splittedArg.Length > 0) + { + Log.Cyan("Excluded assemblies:"); + foreach (string assembly in splittedArg) + { + Log.Cyan($" - {assembly}"); + config.ExcludedAssemblies.Add(assembly); + } + } + else + { + Log.ErrorPrintHelpAndExit("You must specify at least one assembly."); + } + + mode = Mode.Initial; + break; + } + + case Mode.ExcludedNamespaces: + { + string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + + if (splittedArg.Length > 0) + { + Log.Cyan("Excluded namespaces:"); + foreach (string ns in splittedArg) + { + Log.Cyan($" - {ns}"); + config.ExcludedNamespaces.Add(ns); + } + } + else + { + Log.ErrorPrintHelpAndExit("You must specify at least one namespace."); + } + + mode = Mode.Initial; + break; + } + + case Mode.ExcludedTypes: + { + string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + + if (splittedArg.Length > 0) + { + Log.Cyan($"Excluded types:"); + foreach (string typeName in splittedArg) + { + Log.Cyan($" - {typeName}"); + config.ExcludedTypes.Add(typeName); + } + } + else + { + Log.ErrorPrintHelpAndExit("You must specify at least one type name."); + } + + mode = Mode.Initial; + break; + } + + case Mode.IncludedAssemblies: + { + string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + + if (splittedArg.Length > 0) + { + Log.Cyan($"Included assemblies:"); + foreach (string assembly in splittedArg) + { + Log.Cyan($" - {assembly}"); + config.IncludedAssemblies.Add(assembly); + } + } + else + { + Log.ErrorPrintHelpAndExit("You must specify at least one assembly."); + } + + mode = Mode.Initial; + break; + } + + case Mode.IncludedNamespaces: + { + string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + + if (splittedArg.Length > 0) + { + Log.Cyan($"Included namespaces:"); + foreach (string ns in splittedArg) + { + Log.Cyan($" - {ns}"); + config.IncludedNamespaces.Add(ns); + } + } + else + { + Log.ErrorPrintHelpAndExit("You must specify at least one namespace."); + } + + mode = Mode.Initial; + break; + } + + case Mode.IncludedTypes: + { + string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + + if (splittedArg.Length > 0) + { + Log.Cyan($"Included types:"); + foreach (string typeName in splittedArg) + { + Log.Cyan($" - {typeName}"); + config.IncludedTypes.Add(typeName); + } + } + else + { + Log.ErrorPrintHelpAndExit("You must specify at least one type name."); + } + + mode = Mode.Initial; + break; + } + + case Mode.Initial: + { + switch (arg.ToUpperInvariant()) + { + case "-BINLOG": + mode = Mode.BinLog; + break; + + case "-CSPROJ": + mode = Mode.CsProj; + break; + + case "-DIRECTION": + mode = Mode.Direction; + break; + + case "-DOCS": + mode = Mode.Docs; + break; + + case "-DISABLEPROMPTS": + mode = Mode.DisablePrompts; + break; + + case "EXCEPTIONCOLLISIONTHRESHOLD": + mode = Mode.ExceptionCollisionThreshold; + break; + + case "-EXCLUDEDASSEMBLIES": + mode = Mode.ExcludedAssemblies; + break; + + case "-EXCLUDEDNAMESPACES": + mode = Mode.ExcludedNamespaces; + break; + + case "-EXCLUDEDTYPES": + mode = Mode.ExcludedTypes; + break; + + case "-H": + case "-HELP": + Log.PrintHelp(); + Environment.Exit(0); + break; + + case "-INCLUDEDASSEMBLIES": + mode = Mode.IncludedAssemblies; + break; + + case "-INCLUDEDNAMESPACES": + mode = Mode.IncludedNamespaces; + break; + + case "-INCLUDEDTYPES": + mode = Mode.IncludedTypes; + break; + + case "-INTELLISENSE": + mode = Mode.IntelliSense; + break; + + case "-PORTEXCEPTIONSEXISTING": + mode = Mode.PortExceptionsExisting; + break; + + case "-PORTEXCEPTIONSNEW": + mode = Mode.PortExceptionsNew; + break; + + case "-PORTMEMBERPARAMS": + mode = Mode.PortMemberParams; + break; + + case "-PORTMEMBERPROPERTIES": + mode = Mode.PortMemberProperties; + break; + + case "-PORTMEMBERRETURNS": + mode = Mode.PortMemberReturns; + break; + + case "-PORTMEMBERREMARKS": + mode = Mode.PortMemberRemarks; + break; + + case "-PORTMEMBERSUMMARIES": + mode = Mode.PortMemberSummaries; + break; + + case "-PORTMEMBERTYPEPARAMS": + mode = Mode.PortMemberTypeParams; + break; + + case "-PORTTYPEPARAMS": // Params of a Type + mode = Mode.PortTypeParams; + break; + + case "-PORTTYPEREMARKS": + mode = Mode.PortTypeRemarks; + break; + + case "-PORTTYPESUMMARIES": + mode = Mode.PortTypeSummaries; + break; + + case "-PORTTYPETYPEPARAMS": // TypeParams of a Type + mode = Mode.PortTypeTypeParams; + break; + + case "-PRINTUNDOC": + mode = Mode.PrintUndoc; + break; + + case "-SAVE": + mode = Mode.Save; + break; + + case "-SKIPINTERFACEIMPLEMENTATIONS": + mode = Mode.SkipInterfaceImplementations; + break; + + case "-SKIPINTERFACEREMARKS": + mode = Mode.SkipInterfaceRemarks; + break; + + default: + Log.ErrorPrintHelpAndExit($"Unrecognized argument: {arg}"); + break; + } + break; + } + + case Mode.IntelliSense: + { + string[] splittedDirPaths = arg.Split(',', StringSplitOptions.RemoveEmptyEntries); + + Log.Cyan($"Specified IntelliSense locations:"); + foreach (string dirPath in splittedDirPaths) + { + DirectoryInfo dirInfo = new DirectoryInfo(dirPath); + if (!dirInfo.Exists) + { + Log.ErrorAndExit($"This IntelliSense directory does not exist: {dirPath}"); + } + + config.DirsIntelliSense.Add(dirInfo); + Log.Info($" - {dirPath}"); + } + + mode = Mode.Initial; + break; + } + + case Mode.PortExceptionsExisting: + { + config.PortExceptionsExisting = ParseOrExit(arg, "Port existing exceptions"); + mode = Mode.Initial; + break; + } + + case Mode.PortExceptionsNew: + { + config.PortExceptionsNew = ParseOrExit(arg, "Port new exceptions"); + mode = Mode.Initial; + break; + } + + case Mode.PortMemberParams: + { + config.PortMemberParams = ParseOrExit(arg, "Port member Params"); + mode = Mode.Initial; + break; + } + + case Mode.PortMemberProperties: + { + config.PortMemberProperties = ParseOrExit(arg, "Port member Properties"); + mode = Mode.Initial; + break; + } + + case Mode.PortMemberRemarks: + { + config.PortMemberRemarks = ParseOrExit(arg, "Port member Remarks"); + mode = Mode.Initial; + break; + } + + case Mode.PortMemberReturns: + { + config.PortMemberReturns = ParseOrExit(arg, "Port member Returns"); + mode = Mode.Initial; + break; + } + + case Mode.PortMemberSummaries: + { + config.PortMemberSummaries = ParseOrExit(arg, "Port member Summaries"); + mode = Mode.Initial; + break; + } + + case Mode.PortMemberTypeParams: + { + config.PortMemberTypeParams = ParseOrExit(arg, "Port member TypeParams"); + mode = Mode.Initial; + break; + } + + case Mode.PortTypeParams: // Params of a Type + { + config.PortTypeParams = ParseOrExit(arg, "Port Type Params"); + mode = Mode.Initial; + break; + } + + case Mode.PortTypeRemarks: + { + config.PortTypeRemarks = ParseOrExit(arg, "Port Type Remarks"); + mode = Mode.Initial; + break; + } + + case Mode.PortTypeSummaries: + { + config.PortTypeSummaries = ParseOrExit(arg, "Port Type Summaries"); + mode = Mode.Initial; + break; + } + + case Mode.PortTypeTypeParams: // TypeParams of a Type + { + config.PortTypeTypeParams = ParseOrExit(arg, "Port Type TypeParams"); + mode = Mode.Initial; + break; + } + + case Mode.PrintUndoc: + { + config.PrintUndoc = ParseOrExit(arg, "Print undoc"); + mode = Mode.Initial; + break; + } + + case Mode.Save: + { + config.Save = ParseOrExit(arg, "Save"); + mode = Mode.Initial; + break; + } + + case Mode.SkipInterfaceImplementations: + { + config.SkipInterfaceImplementations = ParseOrExit(arg, "Skip interface implementations"); + mode = Mode.Initial; + break; + } + + case Mode.SkipInterfaceRemarks: + { + config.SkipInterfaceRemarks = ParseOrExit(arg, "Skip appending interface remarks"); + mode = Mode.Initial; + break; + } + + default: + { + Log.ErrorPrintHelpAndExit("Unexpected mode."); + break; + } + } + } + + if (mode != Mode.Initial) + { + Log.ErrorPrintHelpAndExit("You missed an argument value."); + } + + if (config.DirsDocsXml == null) + { + Log.ErrorPrintHelpAndExit($"You must specify a path to the dotnet-api-docs xml folder using '-{nameof(Mode.Docs)}'."); + } + + if (config.Direction == PortingDirection.ToDocs) + { + if (config.DirsIntelliSense.Count == 0) + { + Log.ErrorPrintHelpAndExit($"You must specify at least one IntelliSense & DLL folder using '-{nameof(Mode.IntelliSense)}'."); + } + } + + if (config.Direction == PortingDirection.ToTripleSlash) + { + if (config.CsProj == null) + { + Log.ErrorPrintHelpAndExit($"You must specify a *.csproj file using '-{nameof(Mode.CsProj)}'."); + } + } + + if (config.IncludedAssemblies.Count == 0) + { + Log.ErrorPrintHelpAndExit($"You must specify at least one assembly with {nameof(IncludedAssemblies)}."); + } + + return config; + } + + // Tries to parse the user argument string as boolean, and if it fails, exits the program. + private static bool ParseOrExit(string arg, string paramFriendlyName) + { + if (!bool.TryParse(arg, out bool value)) + { + Log.ErrorAndExit($"Invalid boolean value for '{paramFriendlyName}' argument: {arg}"); + } + + Log.Cyan($"{paramFriendlyName}:"); + Log.Info($" - {value}"); + + return value; + } + } +} diff --git a/Libraries/Extensions.cs b/Libraries/Extensions.cs new file mode 100644 index 0000000..8f92ab7 --- /dev/null +++ b/Libraries/Extensions.cs @@ -0,0 +1,42 @@ +#nullable enable +using System.Collections.Generic; + +namespace Libraries +{ + // Provides generic extension methods. + internal static class Extensions + { + // Adds a string to a list of strings if the element is not there yet. The method makes sure to escape unexpected curly brackets to prevent formatting exceptions. + public static void AddIfNotExists(this List list, string element) + { + string cleanedElement = element.Escaped(); + if (!list.Contains(cleanedElement)) + { + list.Add(cleanedElement); + } + } + + // Removes the specified subtrings from another string + public static string RemoveSubstrings(this string oldString, params string[] stringsToRemove) + { + string newString = oldString; + foreach (string toRemove in stringsToRemove) + { + if (newString.Contains(toRemove)) + { + newString = newString.Replace(toRemove, string.Empty); + } + } + return newString; + } + + // Some API DocIDs with types contain "{" and "}" to enclose the typeparam, which causes + // an exception to be thrown when trying to embed the string in a formatted string. + public static string Escaped(this string str) => str.Replace("{", "{{").Replace("}", "}}"); + + // Checks if the passed string is considered "empty" according to the Docs repo rules. + public static bool IsDocsEmpty(this string? s) => + string.IsNullOrWhiteSpace(s) || s == Configuration.ToBeAdded; + } + +} diff --git a/Libraries/Libraries.csproj b/Libraries/Libraries.csproj new file mode 100644 index 0000000..2ad72f9 --- /dev/null +++ b/Libraries/Libraries.csproj @@ -0,0 +1,26 @@ + + + + Library + net5.0 + Microsoft + carlossanlop + enable + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Libraries/Log.cs b/Libraries/Log.cs new file mode 100644 index 0000000..5866fe6 --- /dev/null +++ b/Libraries/Log.cs @@ -0,0 +1,408 @@ +#nullable enable +using System; + +namespace Libraries +{ + internal class Log + { + private static void WriteLine(string format, params object[]? args) + { + if (args == null || args.Length == 0) + { + Console.WriteLine(format); + } + else + { + Console.WriteLine(format, args); + } + } + + private static void Write(string format, params object[]? args) + { + if (args == null || args.Length == 0) + { + Console.Write(format); + } + else + { + Console.Write(format, args); + } + } + + public static void Print(bool endline, ConsoleColor foregroundColor, string format, params object[]? args) + { + ConsoleColor initialColor = Console.ForegroundColor; + Console.ForegroundColor = foregroundColor; + if (endline) + { + WriteLine(format, args); + } + else + { + Write(format, args); + } + Console.ForegroundColor = initialColor; + } + + public static void Info(string format) + { + Info(format, null); + } + + public static void Info(string format, params object[]? args) + { + Info(true, format, args); + } + + public static void Info(bool endline, string format, params object[]? args) + { + Print(endline, ConsoleColor.White, format, args); + } + + public static void Success(string format) + { + Success(format, null); + } + + public static void Success(string format, params object[]? args) + { + Success(true, format, args); + } + + public static void Success(bool endline, string format, params object[]? args) + { + Print(endline, ConsoleColor.Green, format, args); + } + + public static void Warning(string format) + { + Warning(format, null); + } + + public static void Warning(string format, params object[]? args) + { + Warning(true, format, args); + } + + public static void Warning(bool endline, string format, params object[]? args) + { + Print(endline, ConsoleColor.Yellow, format, args); + } + + public static void Error(string format) + { + Error(format, null); + } + + public static void Error(string format, params object[]? args) + { + Error(true, format, args); + } + + public static void Error(bool endline, string format, params object[]? args) + { + Print(endline, ConsoleColor.Red, format, args); + } + + public static void Cyan(string format) + { + Cyan(format, null); + } + + public static void Cyan(string format, params object[]? args) + { + Cyan(true, format, args); + } + + public static void Magenta(bool endline, string format, params object[]? args) + { + Print(endline, ConsoleColor.Magenta, format, args); + } + + public static void Magenta(string format) + { + Magenta(format, null); + } + + public static void Magenta(string format, params object[]? args) + { + Magenta(true, format, args); + } + + public static void Cyan(bool endline, string format, params object[]? args) + { + Print(endline, ConsoleColor.Cyan, format, args); + } + + public static void Assert(bool condition, string format, params object[]? args) + { + Assert(true, condition, format, args); + } + + public static void Assert(bool endline, bool condition, string format, params object[]? args) + { + if (condition) + { + Success(endline, format, args); + } + else + { + Error(endline, format, args); + } + } + + public static void Line() + { + Console.WriteLine(); + } + + public delegate void PrintHelpFunction(); + + public static void ErrorAndExit(string format, params object[]? args) + { + Error(format, args); + Environment.Exit(0); + } + + public static void ErrorPrintHelpAndExit(string format, params object[]? args) + { + PrintHelp(); + Error(format, args); + Environment.Exit(0); + } + + public static void PrintHelp() + { + Cyan(@" +This tool finds and ports triple slash comments found in .NET repos but do not yet exist in the dotnet-api-docs repo. + +The instructions below assume %SourceRepos% is the root folder of all your git cloned projects. + +Options: + + MANDATORY + ------------------------------------------------------------ + | PARAMETER | TYPE | DESCRIPTION | + ------------------------------------------------------------ + + -Docs comma-separated A comma separated list (no spaces) of absolute directory paths where the Docs xml files are located. + The xml files will be searched for recursively. + If any of the segments in the path may contain spaces, make sure to enclose the path in double quotes. + folder paths Known locations: + > Runtime: %SourceRepos%\dotnet-api-docs\xml + > WPF: %SourceRepos%\dotnet-api-docs\xml + > WinForms: %SourceRepos%\dotnet-api-docs\xml + > ASP.NET MVC: %SourceRepos%\AspNetApiDocs\aspnet-mvc\xml + > ASP.NET Core: %SourceRepos%\AspNetApiDocs\aspnet-core\xml + Usage example: + -Docs ""%SourceRepos%\dotnet-api-docs\xml\System.IO.FileSystem\"",%SourceRepos%\AspNetApiDocs\aspnet-mvc\xml + + -IntelliSense comma-separated Mandatory only when using '-Direction ToDocs' to port from IntelliSense xml to Docs. + folder paths A comma separated list (no spaces) of absolute directory paths where we the IntelliSense xml files + are located. Usually it's the 'artifacts/bin' folder in your source code repo. + The IntelliSense xml files will be searched for recursively. You must specify the root folder (usually 'bin'), + which contains all the subfolders whose names are assemblies or namespaces. Only those names specified + with '-IncludedAssemblies' and '-IncludedNamespaces' will be recursed. + If any of the segments in the path may contain spaces, make sure to enclose the path in double quotes. + Known locations: + > Runtime: %SourceRepos%\runtime\artifacts\bin\ + > CoreCLR: %SourceRepos%\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release\IL\ + > WinForms: %SourceRepos%\winforms\artifacts\bin\ + > WPF: %SourceRepos%\wpf\artifacts\bin\ + Usage example: + -IntelliSense ""%SourceRepos%\corefx\artifacts\bin\"",%SourceRepos%\winforms\artifacts\bin\ + + -IncludedAssemblies string list Comma separated list (no spaces) of assemblies to include. + This argument prevents loading everything in the specified folder. + Usage example: + -IncludedAssemblies System.IO,System.Runtime + + IMPORTANT: + Namespaces usually match the assembly name. There are some exceptions, like with types that live in + the System.Runtime assembly. For those cases, make sure to also specify the -IncludedNamespaces argument. + + -CsProj file path Mandatory only when using '-Direction ToTripleSlash' to port from Docs to triple slash comments in source. + An absolute path to a *.csproj file from your repo. Make sure its the src file, not the ref or test file. + Known locations: + > Runtime: %SourceRepos%\runtime\src\libraries\\src\.csproj + > CoreCLR: %SourceRepos%\runtime\src\coreclr\src\System.Private.CoreLib\System.Private.CoreLib.csproj + > WPF: %SourceRepos%\wpf\src\Microsoft.DotNet.Wpf\src\\.csproj + > WinForms: %SourceRepos%\winforms\src\\src\.csproj + > WCF: %SourceRepos%\wcf\src\\ + Usage example: + -SourceCode ""%SourceRepos%\runtime\src\libraries\System.IO.FileSystem\"",%SourceRepos%\runtime\src\coreclr\src\System.Private.CoreLib\ + + OPTIONAL + ------------------------------------------------------------ + | PARAMETER | TYPE | DESCRIPTION | + ------------------------------------------------------------ + + -h | -Help no arguments Displays this help message. If used, all other arguments are ignored and the program exits. + + -BinLog bool Default is false (binlog file generation is disabled). + When set to true, will output a diagnostics binlog file if using '-Direction ToTripleSlash'. + + -Direction string Default is 'ToDocs'. + Determines in which direction the comments should flow. + Possible values: + > ToDocs: Comments are ported from the Intellisense xml files generated in the specified source code repo build, + to the specified Docs repo containing ECMA xml files. + > ToTripleSlash: Comments are ported from the specified Docs repo containint ECMA xml files, + to the triple slash comments on top of each API in the specified source code repo. + Usage example: + -Direction ToTripleSlash + + -DisablePrompts bool Default is false (prompts are disabled). + Avoids prompting the user for input to correct some particular errors. + Usage example: + -DisablePrompts true + + -ExceptionCollisionThreshold int (0-100) Default is 70 (If >=70% of words collide, the string is not ported). + Decides how sensitive the detection of existing exception strings should be. + The tool compares the Docs exception string with the IntelliSense xml exception string. + If the number of words found in the Docs exception is below the specified threshold, + then the IntelliSense Xml string is appended at the end of the Docs string. + The user is expected to verify the value. + The reason for this is that exceptions go through language review, and may contain more + than one root cause (separated by '-or-'), and there is no easy way to know if the string + has already been ported or not. + Usage example: + -ExceptionCollisionThreshold 60 + + -ExcludedAssemblies string list Default is empty (does not ignore any assemblies/namespaces). + Comma separated list (no spaces) of specific .NET assemblies/namespaces to ignore. + Usage example: + -ExcludedAssemblies System.IO.Compression,System.IO.Pipes + + -ExcludedNamespaces string list Default is empty (does not exclude any namespaces from the specified assemblies). + Comma separated list (no spaces) of specific namespaces to exclude from the specified assemblies. + Usage example: + -ExcludedNamespaces System.Runtime.Intrinsics,System.Reflection.Metadata + + -ExcludedTypes string list Default is empty (does not ignore any types). + Comma separated list (no spaces) of names of types to ignore. + Usage example: + -ExcludedTypes ArgumentException,Stream + + -IncludedNamespaces string list Default is empty (includes all namespaces from the specified assemblies). + Comma separated list (no spaces) of specific namespaces to include from the specified assemblies. + Usage example: + -IncludedNamespaces System,System.Data + + -IncludedTypes string list Default is empty (includes all types in the desired assemblies/namespaces). + Comma separated list (no spaces) of specific types to include. + Usage example: + -IncludedTypes FileStream,DirectoryInfo + + -PortExceptionsExisting bool Default is false (does not find and append existing exceptions). + Enable or disable finding, porting and appending summaries from existing exceptions. + Setting this to true can result in a lot of noise because there is + no easy way to detect if an exception summary has been ported already or not, + especially after it went through language review. + See `-ExceptionCollisionThreshold` to set the collision sensitivity. + Usage example: + -PortExceptionsExisting true + + -PortExceptionsNew bool Default is true (ports new exceptions). + Enable or disable finding and porting new exceptions. + Usage example: + -PortExceptionsNew false + + -PortMemberParams bool Default is true (ports Member parameters). + Enable or disable finding and porting Member parameters. + Usage example: + -PortMemberParams false + + -PortMemberProperties bool Default is true (ports Member properties). + Enable or disable finding and porting Member properties. + Usage example: + -PortMemberProperties false + + -PortMemberReturns bool Default is true (ports Member return values). + Enable or disable finding and porting Member return values. + Usage example: + -PortMemberReturns false + + -PortMemberRemarks bool Default is true (ports Member remarks). + Enable or disable finding and porting Member remarks. + Usage example: + -PortMemberRemarks false + + -PortMemberSummaries bool Default is true (ports Member summaries). + Enable or disable finding and porting Member summaries. + Usage example: + -PortMemberSummaries false + + -PortMemberTypeParams bool Default is true (ports Member TypeParams). + Enable or disable finding and porting Member TypeParams. + Usage example: + -PortMemberTypeParams false + + -PortTypeParams bool Default is true (ports Type Params). + Enable or disable finding and porting Type Params. + Usage example: + -PortTypeParams false + + -PortTypeRemarks bool Default is true (ports Type remarks). + Enable or disable finding and porting Type remarks. + Usage example: + -PortTypeRemarks false + + -PortTypeSummaries bool Default is true (ports Type summaries). + Enable or disable finding and porting Type summaries. + Usage example: + -PortTypeSummaries false + + -PortTypeTypeParams bool Default is true (ports Type TypeParams). + Enable or disable finding and porting Type TypeParams. + Usage example: + -PortTypeTypeParams false + + -PrintUndoc bool Default is false (prints a basic summary). + Prints a detailed summary of all the docs APIs that are undocumented. + Usage example: + -PrintUndoc true + + -Save bool Default is false (does not save changes). + Whether you want to save the changes in the dotnet-api-docs xml files. + Usage example: + -Save true + + -SkipInterfaceImplementations bool Default is false (includes interface implementations). + Whether you want the original interface documentation to be considered to fill the + undocumented API's documentation when the API itself does not provide its own documentation. + Setting this to false will include Explicit Interface Implementations as well. + Usage example: + -SkipInterfaceImplementations true + + -SkipInterfaceRemarks bool Default is true (excludes appending interface remarks). + Whether you want interface implementation remarks to be used when the API itself has no remarks. + Very noisy and generally the content in those remarks do not apply to the API that implements + the interface API. + Usage example: + -SkipInterfaceRemarks false + + "); + Warning(@" + tl;dr: To port from IntelliSense xmls to DOcs, specify these parameters: + + -Docs + -IntelliSense [,,...,] + -IncludedAssemblies [,,...] + -Save true + + Example: + DocsPortingTool \ + -Docs D:\dotnet-api-docs\xml \ + -IntelliSense D:\runtime\artifacts\bin\System.IO.FileSystem\ \ + -IncludedAssemblies System.IO.FileSystem,System.Runtime.Intrinsics \ + -Save true +"); + Magenta(@" + Note: + If the names of your assemblies differ from the namespaces wheres your APIs live, specify the -IncludedNamespaces argument too. + + "); + } + } +} \ No newline at end of file diff --git a/Libraries/ToDocsPorter.cs b/Libraries/ToDocsPorter.cs new file mode 100644 index 0000000..38b2d10 --- /dev/null +++ b/Libraries/ToDocsPorter.cs @@ -0,0 +1,896 @@ +#nullable enable +using Libraries.Docs; +using Libraries.IntelliSenseXml; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Libraries +{ + public class ToDocsPorter + { + private readonly Configuration Config; + private readonly DocsCommentsContainer DocsComments; + private readonly IntelliSenseXmlCommentsContainer IntelliSenseXmlComments; + + private readonly List ModifiedFiles = new List(); + private readonly List ModifiedTypes = new List(); + private readonly List ModifiedAPIs = new List(); + private readonly List ProblematicAPIs = new List(); + private readonly List AddedExceptions = new List(); + + private int TotalModifiedIndividualElements = 0; + + public ToDocsPorter(Configuration config) + { + if (config.Direction != Configuration.PortingDirection.ToDocs) + { + throw new InvalidOperationException($"Unexpected porting direction: {config.Direction}"); + } + Config = config; + DocsComments = new DocsCommentsContainer(config); + IntelliSenseXmlComments = new IntelliSenseXmlCommentsContainer(config); + + } + + public void Start() + { + IntelliSenseXmlComments.CollectFiles(); + + if (!IntelliSenseXmlComments.Members.Any()) + { + Log.ErrorAndExit("No IntelliSense xml comments found."); + } + + DocsComments.CollectFiles(); + if (!DocsComments.Types.Any()) + { + Log.ErrorAndExit("No Docs Type APIs found."); + } + + PortMissingComments(); + + PrintUndocumentedAPIs(); + PrintSummary(); + + DocsComments.Save(); + } + + private void PortMissingComments() + { + Log.Info("Looking for IntelliSense xml comments that can be ported..."); + + foreach (DocsType dTypeToUpdate in DocsComments.Types) + { + PortMissingCommentsForType(dTypeToUpdate); + } + + foreach (DocsMember dMemberToUpdate in DocsComments.Members) + { + PortMissingCommentsForMember(dMemberToUpdate); + } + } + + // Tries to find an IntelliSense xml element from which to port documentation for the specified Docs type. + private void PortMissingCommentsForType(DocsType dTypeToUpdate) + { + IntelliSenseXmlMember? tsTypeToPort = IntelliSenseXmlComments.Members.FirstOrDefault(x => x.Name == dTypeToUpdate.DocIdEscaped); + if (tsTypeToPort != null) + { + if (tsTypeToPort.Name == dTypeToUpdate.DocIdEscaped) + { + TryPortMissingSummaryForAPI(dTypeToUpdate, tsTypeToPort, null); + TryPortMissingRemarksForAPI(dTypeToUpdate, tsTypeToPort, null, skipInterfaceRemarks: true); + TryPortMissingParamsForAPI(dTypeToUpdate, tsTypeToPort, null); // Some types, like delegates, have params + TryPortMissingTypeParamsForAPI(dTypeToUpdate, tsTypeToPort, null); // Type names ending with have TypeParams + } + + if (dTypeToUpdate.Changed) + { + ModifiedTypes.AddIfNotExists(dTypeToUpdate.DocId); + ModifiedFiles.AddIfNotExists(dTypeToUpdate.FilePath); + } + } + } + + // Tries to find an IntelliSense xml element from which to port documentation for the specified Docs member. + private void PortMissingCommentsForMember(DocsMember dMemberToUpdate) + { + string docId = dMemberToUpdate.DocIdEscaped; + IntelliSenseXmlMember? tsMemberToPort = IntelliSenseXmlComments.Members.FirstOrDefault(x => x.Name == docId); + TryGetEIIMember(dMemberToUpdate, out DocsMember? interfacedMember); + + if (tsMemberToPort != null || interfacedMember != null) + { + TryPortMissingSummaryForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember); + TryPortMissingRemarksForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember, Config.SkipInterfaceRemarks); + TryPortMissingParamsForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember); + TryPortMissingTypeParamsForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember); + TryPortMissingExceptionsForMember(dMemberToUpdate, tsMemberToPort); + + // Properties sometimes don't have a but have a + if (dMemberToUpdate.MemberType == "Property") + { + TryPortMissingPropertyForMember(dMemberToUpdate, tsMemberToPort, interfacedMember); + } + else if (dMemberToUpdate.MemberType == "Method") + { + TryPortMissingMethodForMember(dMemberToUpdate, tsMemberToPort, interfacedMember); + } + + if (dMemberToUpdate.Changed) + { + ModifiedAPIs.AddIfNotExists(dMemberToUpdate.DocId); + ModifiedFiles.AddIfNotExists(dMemberToUpdate.FilePath); + } + } + } + + // Gets a string indicating if an API is an explicit interface implementation, or empty. + private string GetIsEII(bool isEII) + { + return isEII ? " (EII) " : string.Empty; + } + + // Gets a string indicating if an API was created, otherwise it was modified. + private string GetIsCreated(bool created) + { + return created ? "Created" : "Modified"; + } + + // Attempts to obtain the member of the implemented interface. + private bool TryGetEIIMember(IDocsAPI dApiToUpdate, out DocsMember? interfacedMember) + { + interfacedMember = null; + + if (!Config.SkipInterfaceImplementations && dApiToUpdate is DocsMember member) + { + string interfacedMemberDocId = member.ImplementsInterfaceMember; + if (!string.IsNullOrEmpty(interfacedMemberDocId)) + { + interfacedMember = DocsComments.Members.FirstOrDefault(x => x.DocId == interfacedMemberDocId); + return interfacedMember != null; + } + } + + return false; + } + + // Ports the summary for the specified API if the field is undocumented. + private void TryPortMissingSummaryForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember) + { + if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeSummaries || + dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberSummaries) + { + return; + } + + // Only port if undocumented in MS Docs + if (dApiToUpdate.Summary.IsDocsEmpty()) + { + bool isEII = false; + + string name = string.Empty; + string value = string.Empty; + + // Try to port IntelliSense xml comments + if (tsMemberToPort != null && !tsMemberToPort.Summary.IsDocsEmpty()) + { + dApiToUpdate.Summary = tsMemberToPort.Summary; + name = tsMemberToPort.Name; + value = tsMemberToPort.Summary; + } + // or try to find if it implements a documented interface + else if (interfacedMember != null && !interfacedMember.Summary.IsDocsEmpty()) + { + dApiToUpdate.Summary = interfacedMember.Summary; + isEII = true; + name = interfacedMember.MemberName; + value = interfacedMember.Summary; + } + + if (!value.IsDocsEmpty()) + { + // Any member can have an empty summary + string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} summary: {name.Escaped()} = {value.Escaped()}"; + PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); + TotalModifiedIndividualElements++; + } + } + } + + // Ports the remarks for the specified API if the field is undocumented. + private void TryPortMissingRemarksForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember, bool skipInterfaceRemarks) + { + if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeRemarks || + dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberRemarks) + { + return; + } + + if (dApiToUpdate.Remarks.IsDocsEmpty()) + { + bool isEII = false; + string name = string.Empty; + string value = string.Empty; + + // Try to port IntelliSense xml comments + if (tsMemberToPort != null && !tsMemberToPort.Remarks.IsDocsEmpty()) + { + dApiToUpdate.Remarks = tsMemberToPort.Remarks; + name = tsMemberToPort.Name; + value = tsMemberToPort.Remarks; + } + // or try to find if it implements a documented interface + // which only happens in docs members (types have a null interfacedMember passed) + else if (interfacedMember != null && !interfacedMember.Remarks.IsDocsEmpty()) + { + DocsMember memberToUpdate = (DocsMember)dApiToUpdate; + + // Only attempt to port if the member name is the same as the interfaced member docid without prefix + if (memberToUpdate.MemberName == interfacedMember.DocId[2..]) + { + string dMemberToUpdateTypeDocIdNoPrefix = memberToUpdate.ParentType.DocId[2..]; + string interfacedMemberTypeDocIdNoPrefix = interfacedMember.ParentType.DocId[2..]; + + // Special text for EIIs in Remarks + string eiiMessage = $"This member is an explicit interface member implementation. It can be used only when the instance is cast to an interface.{Environment.NewLine + Environment.NewLine}"; + + string cleanedInterfaceRemarks = string.Empty; + if (!interfacedMember.Remarks.Contains(Configuration.ToBeAdded)) + { + cleanedInterfaceRemarks = interfacedMember.Remarks.RemoveSubstrings("##Remarks", "## Remarks", ""); + } + + // Only port the interface remarks if the user desired that + if (!skipInterfaceRemarks) + { + dApiToUpdate.Remarks = eiiMessage + cleanedInterfaceRemarks; + } + // Otherwise, always add the EII special message + else + { + dApiToUpdate.Remarks = eiiMessage; + } + + name = interfacedMember.MemberName; + value = dApiToUpdate.Remarks; + + isEII = true; + } + } + + if (!value.IsDocsEmpty()) + { + // Any member can have an empty remark + string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} remarks: {name.Escaped()} = {value.Escaped()}"; + PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); + TotalModifiedIndividualElements++; + } + } + } + + // Ports all the parameter descriptions for the specified API if any of them is undocumented. + private void TryPortMissingParamsForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember) + { + if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeParams || + dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberParams) + { + return; + } + + bool created; + bool isEII; + string name; + string value; + + if (tsMemberToPort != null) + { + foreach (DocsParam dParam in dApiToUpdate.Params) + { + if (dParam.Value.IsDocsEmpty()) + { + created = false; + isEII = false; + name = string.Empty; + value = string.Empty; + + IntelliSenseXmlParam? tsParam = tsMemberToPort.Params.FirstOrDefault(x => x.Name == dParam.Name); + + // When not found, it's a bug in Docs (param name not the same as source/ref), so need to ask the user to indicate correct name + if (tsParam == null) + { + ProblematicAPIs.AddIfNotExists($"Param=[{dParam.Name}] in Member DocId=[{dApiToUpdate.DocId}]"); + + if (tsMemberToPort.Params.Count() == 0) + { + ProblematicAPIs.AddIfNotExists($"Param=[{dParam.Name}] in Member DocId=[{dApiToUpdate.DocId}]"); + Log.Warning($" There were no IntelliSense xml comments for param '{dParam.Name}' in {dApiToUpdate.DocId}"); + } + else + { + created = TryPromptParam(dParam, tsMemberToPort, out IntelliSenseXmlParam? newTsParam); + if (newTsParam == null) + { + Log.Error($" There param '{dParam.Name}' was not found in IntelliSense xml for {dApiToUpdate.DocId}"); + } + else + { + // Now attempt to document it + if (!newTsParam.Value.IsDocsEmpty()) + { + // try to port IntelliSense xml comments + dParam.Value = newTsParam.Value; + name = newTsParam.Name; + value = newTsParam.Value; + } + // or try to find if it implements a documented interface + else if (interfacedMember != null) + { + DocsParam? interfacedParam = interfacedMember.Params.FirstOrDefault(x => x.Name == newTsParam.Name || x.Name == dParam.Name); + if (interfacedParam != null) + { + dParam.Value = interfacedParam.Value; + name = interfacedParam.Name; + value = interfacedParam.Value; + isEII = true; + } + } + } + } + } + // Attempt to port + else if (!tsParam.Value.IsDocsEmpty()) + { + // try to port IntelliSense xml comments + dParam.Value = tsParam.Value; + name = tsParam.Name; + value = tsParam.Value; + } + // or try to find if it implements a documented interface + else if (interfacedMember != null) + { + DocsParam? interfacedParam = interfacedMember.Params.FirstOrDefault(x => x.Name == dParam.Name); + if (interfacedParam != null) + { + dParam.Value = interfacedParam.Value; + name = interfacedParam.Name; + value = interfacedParam.Value; + isEII = true; + } + } + + + if (!value.IsDocsEmpty()) + { + string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} ({GetIsCreated(created)}) param {name.Escaped()} = {value.Escaped()}"; + PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); + TotalModifiedIndividualElements++; + } + } + } + } + else if (interfacedMember != null) + { + foreach (DocsParam dParam in dApiToUpdate.Params) + { + if (dParam.Value.IsDocsEmpty()) + { + DocsParam? interfacedParam = interfacedMember.Params.FirstOrDefault(x => x.Name == dParam.Name); + if (interfacedParam != null && !interfacedParam.Value.IsDocsEmpty()) + { + dParam.Value = interfacedParam.Value; + + string message = $"{dApiToUpdate.Kind} EII ({GetIsCreated(false)}) param {dParam.Name.Escaped()} = {dParam.Value.Escaped()}"; + PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); + TotalModifiedIndividualElements++; + } + } + } + } + } + + // Ports all the type parameter descriptions for the specified API if any of them is undocumented. + private void TryPortMissingTypeParamsForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember) + { + if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeTypeParams || + dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberTypeParams) + { + return; + } + + if (tsMemberToPort != null) + { + foreach (IntelliSenseXmlTypeParam tsTypeParam in tsMemberToPort.TypeParams) + { + bool isEII = false; + string name = string.Empty; + string value = string.Empty; + + DocsTypeParam? dTypeParam = dApiToUpdate.TypeParams.FirstOrDefault(x => x.Name == tsTypeParam.Name); + + bool created = false; + if (dTypeParam == null) + { + ProblematicAPIs.AddIfNotExists($"TypeParam=[{tsTypeParam.Name}] in Member=[{dApiToUpdate.DocId}]"); + dTypeParam = dApiToUpdate.AddTypeParam(tsTypeParam.Name, XmlHelper.GetNodesInPlainText(tsTypeParam.XETypeParam)); + created = true; + } + + // But it can still be empty, try to retrieve it + if (dTypeParam.Value.IsDocsEmpty()) + { + // try to port IntelliSense xml comments + if (!tsTypeParam.Value.IsDocsEmpty()) + { + name = tsTypeParam.Name; + value = tsTypeParam.Value; + } + // or try to find if it implements a documented interface + else if (interfacedMember != null) + { + DocsTypeParam? interfacedTypeParam = interfacedMember.TypeParams.FirstOrDefault(x => x.Name == dTypeParam.Name); + if (interfacedTypeParam != null) + { + name = interfacedTypeParam.Name; + value = interfacedTypeParam.Value; + isEII = true; + } + } + } + + if (!value.IsDocsEmpty()) + { + dTypeParam.Value = value; + string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} ({GetIsCreated(created)}) typeparam {name.Escaped()} = {value.Escaped()}"; + PrintModifiedMember(message, dTypeParam.ParentAPI.FilePath, dApiToUpdate.DocId); + TotalModifiedIndividualElements++; + } + } + } + } + + // Tries to document the passed property. + private void TryPortMissingPropertyForMember(DocsMember dMemberToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember) + { + if (!Config.PortMemberProperties) + { + return; + } + + if (dMemberToUpdate.Value.IsDocsEmpty()) + { + string name = string.Empty; + string value = string.Empty; + bool isEII = false; + + // Issue: sometimes properties have their TS string in Value, sometimes in Returns + if (tsMemberToPort != null) + { + name = tsMemberToPort.Name; + if (!tsMemberToPort.Value.IsDocsEmpty()) + { + value = tsMemberToPort.Value; + } + else if (!tsMemberToPort.Returns.IsDocsEmpty()) + { + value = tsMemberToPort.Returns; + } + } + // or try to find if it implements a documented interface + else if (interfacedMember != null) + { + name = interfacedMember.MemberName; + if (!interfacedMember.Value.IsDocsEmpty()) + { + value = interfacedMember.Value; + } + else if (!interfacedMember.Returns.IsDocsEmpty()) + { + value = interfacedMember.Returns; + } + if (!string.IsNullOrEmpty(value)) + { + isEII = true; + } + } + + if (!value.IsDocsEmpty()) + { + dMemberToUpdate.Value = value; + string message = $"Member {GetIsEII(isEII)} property {name.Escaped()} = {value.Escaped()}"; + PrintModifiedMember(message, dMemberToUpdate.FilePath,dMemberToUpdate.DocId); + TotalModifiedIndividualElements++; + } + } + } + + // Tries to document the passed method. + private void TryPortMissingMethodForMember(DocsMember dMemberToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember) + { + if (!Config.PortMemberReturns) + { + return; + } + + if (dMemberToUpdate.Returns.IsDocsEmpty()) + { + string name = string.Empty; + string value = string.Empty; + bool isEII = false; + + // Bug: Sometimes a void return value shows up as not documented, skip those + if (dMemberToUpdate.ReturnType == "System.Void") + { + ProblematicAPIs.AddIfNotExists($"Unexpected System.Void return value in Method=[{dMemberToUpdate.DocId}]"); + } + else if (tsMemberToPort != null && !tsMemberToPort.Returns.IsDocsEmpty()) + { + name = tsMemberToPort.Name; + value = tsMemberToPort.Returns; + } + else if (interfacedMember != null && !interfacedMember.Returns.IsDocsEmpty()) + { + name = interfacedMember.MemberName; + value = interfacedMember.Returns; + isEII = true; + } + + if (!value.IsDocsEmpty()) + { + dMemberToUpdate.Returns = value; + string message = $"Method {GetIsEII(isEII)} returns {name.Escaped()} = {value.Escaped()}"; + PrintModifiedMember(message, dMemberToUpdate.FilePath, dMemberToUpdate.DocId); + TotalModifiedIndividualElements++; + } + } + } + + // Ports all the exceptions for the specified API. + // They are only processed if the user specified in the command arguments to NOT skip exceptions. + // All exceptions get ported, because there is no easy way to determine if an exception is already documented or not. + private void TryPortMissingExceptionsForMember(DocsMember dMemberToUpdate, IntelliSenseXmlMember? tsMemberToPort) + { + if (!Config.PortExceptionsExisting && !Config.PortExceptionsNew) + { + return; + } + + if (tsMemberToPort != null) + { + // Exceptions are a special case: If a new one is found in code, but does not exist in docs, the whole element needs to be added + foreach (IntelliSenseXmlException tsException in tsMemberToPort.Exceptions) + { + DocsException? dException = dMemberToUpdate.Exceptions.FirstOrDefault(x => x.Cref == tsException.Cref); + bool created = false; + + // First time adding the cref + if (dException == null && Config.PortExceptionsNew) + { + AddedExceptions.AddIfNotExists($"Exception=[{tsException.Cref}] in Member=[{dMemberToUpdate.DocId}]"); + string text = XmlHelper.ReplaceExceptionPatterns(XmlHelper.GetNodesInPlainText(tsException.XEException)); + dException = dMemberToUpdate.AddException(tsException.Cref, text); + created = true; + } + // If cref exists, check if the text has already been appended + else if (dException != null && Config.PortExceptionsExisting) + { + XElement formattedException = tsException.XEException; + string value = XmlHelper.ReplaceExceptionPatterns(XmlHelper.GetNodesInPlainText(formattedException)); + if (!dException.WordCountCollidesAboveThreshold(value, Config.ExceptionCollisionThreshold)) + { + AddedExceptions.AddIfNotExists($"Exception=[{tsException.Cref}] in Member=[{dMemberToUpdate.DocId}]"); + dException.AppendException(value); + created = true; + } + } + + if (dException != null) + { + if (created || (!tsException.Value.IsDocsEmpty() && dException.Value.IsDocsEmpty())) + { + string message = string.Format($"Exception ({GetIsCreated(created)}) {dException.Cref.Escaped()} = {dException.Value.Escaped()}"); + PrintModifiedMember(message, dException.ParentAPI.FilePath, dException.Cref); + + TotalModifiedIndividualElements++; + } + } + } + } + } + + // If a Param is found in a DocsType or a DocsMember that did not exist in the IntelliSense xml member, it's possible the param was unexpectedly saved in the IntelliSense xml comments with a different name, so the user gets prompted to look for it. + private bool TryPromptParam(DocsParam oldDParam, IntelliSenseXmlMember tsMember, out IntelliSenseXmlParam? newTsParam) + { + newTsParam = null; + + if (Config.DisablePrompts) + { + Log.Error($"Prompts disabled. Will not process the '{oldDParam.Name}' param."); + return false; + } + + bool created = false; + int option = -1; + while (option == -1) + { + Log.Error($"Problem in param '{oldDParam.Name}' in member '{tsMember.Name}' in file '{oldDParam.ParentAPI.FilePath}'"); + Log.Error($"The param probably exists in code, but the exact name was not found in Docs. What would you like to do?"); + Log.Warning(" 0 - Exit program."); + Log.Info(" 1 - Select the correct IntelliSense xml param from the existing ones."); + Log.Info(" 2 - Ignore this param."); + Log.Warning(" Note:Make sure to double check the affected Docs file after the tool finishes executing."); + Log.Cyan(false, "Your answer [0,1,2]: "); + + if (!int.TryParse(Console.ReadLine(), out option)) + { + Log.Error("Not a number. Try again."); + option = -1; + } + else + { + switch (option) + { + case 0: + { + Log.Info("Goodbye!"); + Environment.Exit(0); + break; + } + + case 1: + { + int paramSelection = -1; + while (paramSelection == -1) + { + Log.Info($"IntelliSense xml params found in member '{tsMember.Name}':"); + Log.Warning(" 0 - Exit program."); + int paramCounter = 1; + foreach (IntelliSenseXmlParam param in tsMember.Params) + { + Log.Info($" {paramCounter} - {param.Name}"); + paramCounter++; + } + + Log.Cyan(false, $"Your answer to match param '{oldDParam.Name}'? [0..{paramCounter - 1}]: "); + + if (!int.TryParse(Console.ReadLine(), out paramSelection)) + { + Log.Error("Not a number. Try again."); + paramSelection = -1; + } + else if (paramSelection < 0 || paramSelection >= paramCounter) + { + Log.Error("Invalid selection. Try again."); + paramSelection = -1; + } + else if (paramSelection == 0) + { + Log.Info("Goodbye!"); + Environment.Exit(0); + } + else + { + newTsParam = tsMember.Params[paramSelection - 1]; + Log.Success($"Selected: {newTsParam.Name}"); + } + } + + break; + } + + case 2: + { + Log.Info("Skipping this param."); + break; + } + + default: + { + Log.Error("Invalid selection. Try again."); + option = -1; + break; + } + } + } + } + + return created; + } + + /// + /// Standard formatted print message for a modified element. + /// + /// The friendly description of the modified API. + /// The file where the modified API lives. + /// The API unique identifier. + private void PrintModifiedMember(string message, string docsFilePath, string docId) + { + Log.Warning($" File: {docsFilePath}"); + Log.Warning($" DocID: {docId}"); + Log.Warning($" {message}"); + Log.Info("---------------------------------------------------"); + Log.Line(); + } + + // Prints all the undocumented APIs. + // This is only done if the user specified in the command arguments to print undocumented APIs. + private void PrintUndocumentedAPIs() + { + if (Config.PrintUndoc) + { + Log.Line(); + Log.Success("-----------------"); + Log.Success("UNDOCUMENTED APIS"); + Log.Success("-----------------"); + + Log.Line(); + + void TryPrintType(ref bool undocAPI, string typeDocId) + { + if (!undocAPI) + { + Log.Info(" Type: {0}", typeDocId); + undocAPI = true; + } + }; + + void TryPrintMember(ref bool undocMember, string memberDocId) + { + if (!undocMember) + { + Log.Info(" {0}", memberDocId); + undocMember = true; + } + }; + + int typeSummaries = 0; + int memberSummaries = 0; + int memberValues = 0; + int memberReturns = 0; + int memberParams = 0; + int memberTypeParams = 0; + int exceptions = 0; + + Log.Info("Undocumented APIs:"); + + foreach (DocsType docsType in DocsComments.Types) + { + bool undocAPI = false; + if (docsType.Summary.IsDocsEmpty()) + { + TryPrintType(ref undocAPI, docsType.DocId); + Log.Error($" Type Summary: {docsType.Summary}"); + typeSummaries++; + } + } + + foreach (DocsMember member in DocsComments.Members) + { + bool undocMember = false; + + if (member.Summary.IsDocsEmpty()) + { + TryPrintMember(ref undocMember, member.DocId); + + Log.Error($" Member Summary: {member.Summary}"); + memberSummaries++; + } + + if (member.MemberType == "Property") + { + if (member.Value == Configuration.ToBeAdded) + { + TryPrintMember(ref undocMember, member.DocId); + + Log.Error($" Property Value: {member.Value}"); + memberValues++; + } + } + else if (member.MemberType == "Method") + { + if (member.Returns == Configuration.ToBeAdded) + { + TryPrintMember(ref undocMember, member.DocId); + + Log.Error($" Method Returns: {member.Returns}"); + memberReturns++; + } + } + + foreach (DocsParam param in member.Params) + { + if (param.Value.IsDocsEmpty()) + { + TryPrintMember(ref undocMember, member.DocId); + + Log.Error($" Member Param: {param.Name}: {param.Value}"); + memberParams++; + } + } + + foreach (DocsTypeParam typeParam in member.TypeParams) + { + if (typeParam.Value.IsDocsEmpty()) + { + TryPrintMember(ref undocMember, member.DocId); + + Log.Error($" Member Type Param: {typeParam.Name}: {typeParam.Value}"); + memberTypeParams++; + } + } + + foreach (DocsException exception in member.Exceptions) + { + if (exception.Value.IsDocsEmpty()) + { + TryPrintMember(ref undocMember, member.DocId); + + Log.Error($" Member Exception: {exception.Cref}: {exception.Value}"); + exceptions++; + } + } + } + + Log.Info($" Undocumented type summaries: {typeSummaries}"); + Log.Info($" Undocumented member summaries: {memberSummaries}"); + Log.Info($" Undocumented method returns: {memberReturns}"); + Log.Info($" Undocumented property values: {memberValues}"); + Log.Info($" Undocumented member params: {memberParams}"); + Log.Info($" Undocumented member type params: {memberTypeParams}"); + Log.Info($" Undocumented exceptions: {exceptions}"); + + Log.Line(); + } + } + + // Prints a final summary of the execution findings. + private void PrintSummary() + { + Log.Line(); + Log.Success("---------"); + Log.Success("FINISHED!"); + Log.Success("---------"); + + Log.Line(); + Log.Info($"Total modified files: {ModifiedFiles.Count}"); + foreach (string file in ModifiedFiles) + { + Log.Success($" - {file}"); + } + + Log.Line(); + Log.Info($"Total modified types: {ModifiedTypes.Count}"); + foreach (string type in ModifiedTypes) + { + Log.Success($" - {type}"); + } + + Log.Line(); + Log.Info($"Total modified APIs: {ModifiedAPIs.Count}"); + foreach (string api in ModifiedAPIs) + { + Log.Success($" - {api}"); + } + + Log.Line(); + Log.Info($"Total problematic APIs: {ProblematicAPIs.Count}"); + foreach (string api in ProblematicAPIs) + { + Log.Warning($" - {api}"); + } + + Log.Line(); + Log.Info($"Total added exceptions: {AddedExceptions.Count}"); + foreach (string exception in AddedExceptions) + { + Log.Success($" - {exception}"); + } + + Log.Line(); + Log.Info(false, "Total modified individual elements: "); + Log.Success($"{TotalModifiedIndividualElements}"); + } + } +} diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs new file mode 100644 index 0000000..382c84b --- /dev/null +++ b/Libraries/ToTripleSlashPorter.cs @@ -0,0 +1,133 @@ +#nullable enable +using Libraries.Docs; +using Libraries.RoslynTripleSlash; +using Microsoft.Build.Logging; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.MSBuild; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Libraries +{ + public class ToTripleSlashPorter + { + private readonly Configuration Config; + private readonly DocsCommentsContainer DocsComments; + + public ToTripleSlashPorter(Configuration config) + { + if (config.Direction != Configuration.PortingDirection.ToTripleSlash) + { + throw new InvalidOperationException($"Unexpected porting direction: {config.Direction}"); + } + Config = config; + DocsComments = new DocsCommentsContainer(config); + } + + public void Start() + { + DocsComments.CollectFiles(); + if (!DocsComments.Types.Any()) + { + Log.ErrorAndExit("No Docs Type APIs found."); + } + + Log.Info("Porting from Docs to triple slash..."); + + MSBuildWorkspace workspace; + try + { + workspace = MSBuildWorkspace.Create(); + } + catch (ReflectionTypeLoadException) + { + Log.ErrorAndExit("The MSBuild directory was not found in PATH. Use '-MSBuild ' to specify it."); + return; + } + + BinaryLogger? binLogger = null; + if (Config.BinLogger) + { + binLogger = new BinaryLogger() + { + Parameters = Path.Combine(Environment.CurrentDirectory, Config.BinLogPath), + Verbosity = Microsoft.Build.Framework.LoggerVerbosity.Diagnostic, + CollectProjectImports = BinaryLogger.ProjectImportsCollectionMode.Embed + }; + } + + Project? project = workspace.OpenProjectAsync(Config.CsProj!.FullName, msbuildLogger: binLogger).Result; + if (project == null) + { + Log.ErrorAndExit("Could not find a project."); + return; + } + + Compilation? compilation = project.GetCompilationAsync().Result; + if (compilation == null) + { + throw new NullReferenceException("The project's compilation was null."); + } + + ImmutableList diagnostics = workspace.Diagnostics; + if (diagnostics.Any()) + { + foreach (var diagnostic in diagnostics) + { + Log.Error($"{diagnostic.Kind} - {diagnostic.Message}"); + } + Log.ErrorAndExit("Exiting due to diagnostic errors found."); + } + + PortCommentsForAPIs(compilation!); + } + + private void PortCommentsForAPIs(Compilation compilation) + { + foreach (DocsType docsType in DocsComments.Types) + { + INamedTypeSymbol? typeSymbol = + compilation.GetTypeByMetadataName(docsType.FullName) ?? + compilation.Assembly.GetTypeByMetadataName(docsType.FullName); + + if (typeSymbol == null) + { + Log.Warning($"Type symbol not found in compilation: {docsType.DocId}"); + continue; + } + + PortAPI(compilation, docsType, typeSymbol); + } + } + + private void PortAPI(Compilation compilation, IDocsAPI api, ISymbol symbol) + { + bool useBoilerplate = false; + foreach (Location location in symbol.Locations) + { + SyntaxTree? tree = location.SourceTree; + if (tree == null) + { + Log.Warning($"Tree not found for location of {symbol.Name}"); + continue; + } + + SemanticModel model = compilation.GetSemanticModel(tree); + var rewriter = new TripleSlashSyntaxRewriter(DocsComments, model, location, tree, useBoilerplate); + SyntaxNode? newRoot = rewriter.Visit(tree.GetRoot()); + if (newRoot == null) + { + Log.Warning($"New returned root is null for {api.DocId} in {tree.FilePath}"); + continue; + } + + File.WriteAllText(tree.FilePath, newRoot.ToFullString()); + useBoilerplate = true; + } + } + } +} diff --git a/Libraries/XmlHelper.cs b/Libraries/XmlHelper.cs new file mode 100644 index 0000000..cbf6a6c --- /dev/null +++ b/Libraries/XmlHelper.cs @@ -0,0 +1,320 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Linq; + +namespace Libraries +{ + 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" }, + }; + + public static string GetAttributeValue(XElement parent, string name) + { + if (parent == null) + { + Log.Error("A null parent was passed when attempting to get attribute '{0}'", name); + throw new ArgumentNullException(nameof(parent)); + } + else + { + XAttribute attr = parent.Attribute(name); + if (attr != null) + { + return attr.Value.Trim(); + } + } + return string.Empty; + } + + public static bool TryGetChildElement(XElement parent, string name, out XElement? child) + { + child = null; + + if (parent == null || string.IsNullOrWhiteSpace(name)) + return false; + + child = parent.Element(name); + + return child != null; + } + + public static string GetChildElementValue(XElement parent, string childName) + { + XElement child = parent.Element(childName); + + if (child != null) + { + return GetNodesInPlainText(child); + } + + return string.Empty; + } + + public static string GetNodesInPlainText(XElement element) + { + if (element == null) + { + Log.Error("A null element was passed when attempting to retrieve the nodes in plain text."); + throw new ArgumentNullException(nameof(element)); + } + return string.Join("", element.Nodes()).Trim(); + } + + public static void SaveFormattedAsMarkdown(XElement element, string newValue, bool isMember) + { + if (element == null) + { + Log.Error("A null element was passed when attempting to save formatted as markdown"); + throw new ArgumentNullException(nameof(element)); + } + + // 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 = RemoveUndesiredEndlines(newValue); + updatedValue = SubstituteRemarksRegexPatterns(updatedValue); + updatedValue = ReplaceMarkdownPatterns(updatedValue); + + 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) + { + Log.Error("A null parent was passed when attempting to add child formatted as markdown"); + throw new ArgumentNullException(nameof(parent)); + } + + if (child == null) + { + Log.Error("A null child was passed when attempting to add child formatted as markdown"); + throw new ArgumentNullException(nameof(child)); + } + + SaveFormattedAsMarkdown(child, childValue, isMember); + parent.Add(child); + } + + public static void SaveFormattedAsXml(XElement element, string newValue, bool removeUndesiredEndlines = true) + { + if (element == null) + { + Log.Error("A null element was passed when attempting to save formatted as xml"); + throw new ArgumentNullException(nameof(element)); + } + + 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) + { + Log.Error("A null element was passed when attempting to append formatted as xml"); + throw new ArgumentNullException(nameof(element)); + } + + SaveFormattedAsXml(element, GetNodesInPlainText(element) + valueToAppend, removeUndesiredEndlines); + } + + public static void AddChildFormattedAsXml(XElement parent, XElement child, string childValue) + { + if (parent == null) + { + Log.Error("A null parent was passed when attempting to add child formatted as xml"); + throw new ArgumentNullException(nameof(parent)); + } + + if (child == null) + { + Log.Error("A null child was passed when attempting to add child formatted as xml"); + throw new ArgumentNullException(nameof(child)); + } + + SaveFormattedAsXml(child, childValue); + parent.Add(child); + } + + private static string RemoveUndesiredEndlines(string value) + { + Regex regex = new Regex(@"((?'undesiredEndlinePrefix'[^\.\:])(\r\n)+[ \t]*)"); + string newValue = value; + if (regex.IsMatch(value)) + { + newValue = regex.Replace(value, @"${undesiredEndlinePrefix} "); + } + return newValue.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 (updatedValue.Contains(kvp.Key)) + { + updatedValue = updatedValue.Replace(kvp.Key, kvp.Value); + } + } + 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); + } + } + 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; + } + + private static string SubstituteRegexPatterns(string value, Dictionary replaceableRegexPatterns) + { + foreach (KeyValuePair pattern in replaceableRegexPatterns) + { + Regex regex = new Regex(pattern.Key); + if (regex.IsMatch(value)) + { + value = regex.Replace(value, pattern.Value); + } + } + + return value; + } + } +} \ No newline at end of file From 14cd186ccf6893093f4cd3409c62e966bd24fd18 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:45:02 -0800 Subject: [PATCH 06/65] DocsPortingTool changes. --- Program/DocsPortingTool.cs | 129 +++++++++++++++++++++++++ Program/DocsPortingTool.csproj | 26 +++++ Program/Properties/launchSettings.json | 15 +++ 3 files changed, 170 insertions(+) create mode 100644 Program/DocsPortingTool.cs create mode 100644 Program/DocsPortingTool.csproj create mode 100644 Program/Properties/launchSettings.json diff --git a/Program/DocsPortingTool.cs b/Program/DocsPortingTool.cs new file mode 100644 index 0000000..92d1c70 --- /dev/null +++ b/Program/DocsPortingTool.cs @@ -0,0 +1,129 @@ +#nullable enable +using Libraries; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Loader; +using System.IO; +using System.Linq; +using Microsoft.Build.Locator; + +namespace DocsPortingTool +{ + class DocsPortingTool + { + public static void Main(string[] args) + { + Configuration config = Configuration.GetCLIArgumentsForDocsPortingTool(args); + switch (config.Direction) + { + case Configuration.PortingDirection.ToDocs: + { + var porter = new ToDocsPorter(config); + porter.Start(); + break; + } + case Configuration.PortingDirection.ToTripleSlash: + { + // This ensures we can load MSBuild property before calling the ToTripleSlashPorter constructor + VisualStudioInstance? msBuildInstance = MSBuildLocator.QueryVisualStudioInstances().First(); + Register(msBuildInstance.MSBuildPath); + MSBuildLocator.RegisterInstance(msBuildInstance); + + var porter = new ToTripleSlashPorter(config); + porter.Start(); + break; + } + default: + throw new ArgumentOutOfRangeException($"Unrecognized porting direction: {config.Direction}"); + } + } + + private static readonly Dictionary s_pathsToAssemblies = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary s_namesToAssemblies = new Dictionary(); + + private static readonly object s_guard = new object(); + + /// + /// Register an assembly loader that will load assemblies with higher version than what was requested. + /// + private static void Register(string searchPath) + { + AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext context, AssemblyName assemblyName) => + { + lock (s_guard) + { + if (s_namesToAssemblies.TryGetValue(assemblyName.FullName, out var cachedAssembly)) + { + return cachedAssembly; + } + + var assembly = TryResolveAssemblyFromPaths(context, assemblyName, searchPath, s_pathsToAssemblies); + + // Cache assembly + if (assembly != null) + { + var name = assembly.FullName; + if (name is null) + { + throw new Exception($"Could not get name for assembly '{assembly}'"); + } + + s_pathsToAssemblies[assembly.Location] = assembly; + s_namesToAssemblies[name] = assembly; + } + + return assembly; + } + }; + } + internal static Assembly? TryResolveAssemblyFromPaths(AssemblyLoadContext context, AssemblyName assemblyName, string searchPath, Dictionary? knownAssemblyPaths = null) + { + foreach (var cultureSubfolder in string.IsNullOrEmpty(assemblyName.CultureName) + // If no culture is specified, attempt to load directly from + // the known dependency paths. + ? new[] { string.Empty } + // Search for satellite assemblies in culture subdirectories + // of the assembly search directories, but fall back to the + // bare search directory if that fails. + : new[] { assemblyName.CultureName, string.Empty }) + { + foreach (var extension in new[] { "ni.dll", "ni.exe", "dll", "exe" }) + { + var candidatePath = Path.Combine( + searchPath, cultureSubfolder, $"{assemblyName.Name}.{extension}"); + + var isAssemblyLoaded = knownAssemblyPaths?.ContainsKey(candidatePath) == true; + if (isAssemblyLoaded || !File.Exists(candidatePath)) + { + continue; + } + + var candidateAssemblyName = AssemblyLoadContext.GetAssemblyName(candidatePath); + if (candidateAssemblyName.Version < assemblyName.Version) + { + continue; + } + + try + { + var assembly = context.LoadFromAssemblyPath(candidatePath); + return assembly; + } + catch + { + if (assemblyName.Name != null) + { + // We were unable to load the assembly from the file path. It is likely that + // a different version of the assembly has already been loaded into the context. + // Be forgiving and attempt to load assembly by name without specifying a version. + return context.LoadFromAssemblyName(new AssemblyName(assemblyName.Name)); + } + } + } + } + + return null; + } + } +} diff --git a/Program/DocsPortingTool.csproj b/Program/DocsPortingTool.csproj new file mode 100644 index 0000000..6efdea0 --- /dev/null +++ b/Program/DocsPortingTool.csproj @@ -0,0 +1,26 @@ + + + + Exe + net5.0 + DocsPortingTool.DocsPortingTool + Microsoft + carlossanlop + enable + true + true + 3.0.0 + + + + + + + + + + + + + + diff --git a/Program/Properties/launchSettings.json b/Program/Properties/launchSettings.json new file mode 100644 index 0000000..35956d8 --- /dev/null +++ b/Program/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "profiles": { + "Program": { + "commandName": "Project", + "commandLineArgs": "-CsProj D:\\runtime\\src\\libraries\\System.IO.Compression\\src\\System.IO.Compression.csproj -Docs D:\\dotnet-api-docs\\xml -Save true -Direction ToTripleSlash -IncludedAssemblies System.IO.Compression,System.IO.Compression.Brotli -SkipInterfaceImplementations true", + "environmentVariables": { + "DOCS_IOT": "D:\\iot\\artifacts\\bin", + "DOCS_CORECLR": "D:\\runtime\\artifacts\\bin\\coreclr\\Windows_NT.x64.Release\\IL\\", + "DOCS_WINFORMS": "D:\\winforms\\artifacts\\bin\\", + "DOCS_WPF": "D:\\wpf\\.tools\\native\\bin\\dotnet-api-docs_netcoreapp3.0\\0.0.0.1\\_intellisense\\\\netcore-3.0\\", + "DOCS_RUNTIME": "D:\\runtime\\artifacts\\bin\\" + } + } + } +} \ No newline at end of file From e4476c9ae81296584738b4bc0043a79a98a38d0e Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:45:16 -0800 Subject: [PATCH 07/65] Solution file. --- DocsPortingTool.sln | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/DocsPortingTool.sln b/DocsPortingTool.sln index 4130fb8..84e75b9 100644 --- a/DocsPortingTool.sln +++ b/DocsPortingTool.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28705.295 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocsPortingTool", "DocsPortingTool\DocsPortingTool.csproj", "{87BBF4FD-260C-4AC4-802B-7D2B29629C07}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libraries", "Libraries\Libraries.csproj", "{87BBF4FD-260C-4AC4-802B-7D2B29629C07}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{81FEFEA4-8FF5-482E-A33D-D3F351D3F7B6}" EndProject @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocsPortingTool", "Program\DocsPortingTool.csproj", "{E92246CD-548D-4C08-BA43-594663E78100}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {81FEFEA4-8FF5-482E-A33D-D3F351D3F7B6}.Debug|Any CPU.Build.0 = Debug|Any CPU {81FEFEA4-8FF5-482E-A33D-D3F351D3F7B6}.Release|Any CPU.ActiveCfg = Release|Any CPU {81FEFEA4-8FF5-482E-A33D-D3F351D3F7B6}.Release|Any CPU.Build.0 = Release|Any CPU + {E92246CD-548D-4C08-BA43-594663E78100}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E92246CD-548D-4C08-BA43-594663E78100}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E92246CD-548D-4C08-BA43-594663E78100}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E92246CD-548D-4C08-BA43-594663E78100}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 1bcb3a669eb8ffb36518df977695fd94e68d6ba5 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:45:25 -0800 Subject: [PATCH 08/65] install as tool update. --- install-as-tool.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install-as-tool.ps1 b/install-as-tool.ps1 index cb83c32..dabcd79 100755 --- a/install-as-tool.ps1 +++ b/install-as-tool.ps1 @@ -4,7 +4,7 @@ $ErrorActionPreference = "Stop" Push-Location $(Split-Path $MyInvocation.MyCommand.Path) $ARTIFACTS_DIR = "artifacts" -$PROJECT_NAME = "DocsPortingTool" +$PROJECT_NAME = "Program" $BUILD_CONFIGURATION = "Release" dotnet pack -c $BUILD_CONFIGURATION -o $ARTIFACTS_DIR $PROJECT_NAME From 1588ea6ca1ee57f6c9c62e0bc7e11ee9422734e8 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:45:31 -0800 Subject: [PATCH 09/65] Unit tests. --- Tests/TestData.cs | 10 +++++----- Tests/TestDirectory.cs | 2 +- Tests/Tests.cs | 41 ++++++++++++++++++++--------------------- Tests/Tests.csproj | 18 ++++++++++++------ 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/Tests/TestData.cs b/Tests/TestData.cs index 51157d8..846feef 100644 --- a/Tests/TestData.cs +++ b/Tests/TestData.cs @@ -1,7 +1,7 @@ using System.IO; using Xunit; -namespace DocsPortingTool.Tests +namespace Libraries.Tests { public class TestData { @@ -14,10 +14,10 @@ public class TestData public string Assembly { get; private set; } public string Namespace { get; private set; } public string Type { get; private set; } - public DirectoryInfo TripleSlash { get; private set; } + public DirectoryInfo IntelliSenseAndDLL { get; private set; } public DirectoryInfo Docs { get; private set; } - /// Triple slash xml file. + /// IntelliSense xml file. public string OriginalFilePath { get; private set; } /// Docs file as we should expect it to look. public string ExpectedFilePath { get; private set; } @@ -35,8 +35,8 @@ public TestData(TestDirectory tempDir, string testDataDir, string assemblyName, Namespace = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; Type = typeName; - TripleSlash = tempDir.CreateSubdirectory("TripleSlash"); - DirectoryInfo tsAssemblyDir = TripleSlash.CreateSubdirectory(Assembly); + IntelliSenseAndDLL = tempDir.CreateSubdirectory("IntelliSenseAndDLL"); + DirectoryInfo tsAssemblyDir = IntelliSenseAndDLL.CreateSubdirectory(Assembly); Docs = tempDir.CreateSubdirectory("Docs"); DirectoryInfo docsAssemblyDir = Docs.CreateSubdirectory(Namespace); diff --git a/Tests/TestDirectory.cs b/Tests/TestDirectory.cs index 75a1961..46e0a2b 100644 --- a/Tests/TestDirectory.cs +++ b/Tests/TestDirectory.cs @@ -2,7 +2,7 @@ using System.IO; using Xunit; -namespace DocsPortingTool.Tests +namespace Libraries.Tests { public class TestDirectory : IDisposable { diff --git a/Tests/Tests.cs b/Tests/Tests.cs index 92a3231..8f8c6fc 100644 --- a/Tests/Tests.cs +++ b/Tests/Tests.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; using System.IO; using Xunit; -namespace DocsPortingTool.Tests +namespace Libraries.Tests { public class Tests { @@ -10,60 +9,60 @@ public class Tests // Verifies the basic case of porting all regular fields. public void Port_Basic() { - Port("Basic"); + PortToDocs("Basic"); } [Fact] public void Port_DontAddMissingRemarks() { - Port("DontAddMissingRemarks"); + PortToDocs("DontAddMissingRemarks"); } [Fact] // Verifies porting of APIs living in namespaces whose name match their assembly. public void Port_AssemblyAndNamespaceSame() { - Port("AssemblyAndNamespaceSame"); + PortToDocs("AssemblyAndNamespaceSame"); } [Fact] // Verifies porting of APIs living in namespaces whose name does not match their assembly. public void Port_AssemblyAndNamespaceDifferent() { - Port("AssemblyAndNamespaceDifferent", + PortToDocs("AssemblyAndNamespaceDifferent", assemblyName: "MyAssembly", namespaceName: "MyNamespace"); } [Fact] - // Ports Type remarks from triple slash. - // Ports Method remarks from triple slash. + // Ports Type remarks from IntelliSense xml. + // Ports Method remarks from IntelliSense xml. // No interface strings should be ported. public void Port_Remarks_NoEII_NoInterfaceRemarks() { - Port("Remarks_NoEII_NoInterfaceRemarks", + PortToDocs("Remarks_NoEII_NoInterfaceRemarks", skipInterfaceImplementations: true, skipInterfaceRemarks: true); } [Fact] - // Ports Type remarks from triple slash. - // Ports Method remarks from triple slash. + // Ports Type remarks from IntelliSense xml. + // Ports Method remarks from IntelliSense xml. // Ports EII message and interface method remarks. public void Port_Remarks_WithEII_WithInterfaceRemarks() { - Port("Remarks_WithEII_WithInterfaceRemarks", + PortToDocs("Remarks_WithEII_WithInterfaceRemarks", skipInterfaceImplementations: false, skipInterfaceRemarks: false); } [Fact] - // Ports Type remarks from triple slash. - // Ports Method remarks from triple slash. + // Ports Type remarks from IntelliSense xml. + // Ports Method remarks from IntelliSense xml. // Ports EII message but no interface method remarks. public void Port_Remarks_WithEII_NoInterfaceRemarks() { - Port("Remarks_WithEII_NoInterfaceRemarks", + PortToDocs("Remarks_WithEII_NoInterfaceRemarks", skipInterfaceImplementations: false, skipInterfaceRemarks: true); } @@ -72,7 +71,7 @@ public void Port_Remarks_WithEII_NoInterfaceRemarks() /// Verifies that new exceptions are ported. public void Port_Exceptions() { - Port("Exceptions"); + PortToDocs("Exceptions"); } [Fact] @@ -80,12 +79,12 @@ public void Port_Exceptions() /// language review, does not get ported if its above the difference threshold. public void Port_Exception_ExistingCref() { - Port("Exception_ExistingCref", + PortToDocs("Exception_ExistingCref", portExceptionsExisting: true, exceptionCollisionThreshold: 60); } - private void Port( + private void PortToDocs( string testDataDir, bool disablePrompts = true, bool printUndoc = false, @@ -132,10 +131,10 @@ private void Port( } c.DirsDocsXml.Add(testData.Docs); - c.DirsTripleSlashXmls.Add(testData.TripleSlash); + c.DirsIntelliSense.Add(testData.IntelliSenseAndDLL); - Analyzer analyzer = new Analyzer(c); - analyzer.Start(); + var porter = new ToDocsPorter(c); + porter.Start(); Verify(testData); } diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 829934b..5b5a794 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -7,15 +7,21 @@ - - - - - + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + From f821775e1360a80d96ff3b94f2069935054c9221 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 20 Nov 2020 17:33:25 -0800 Subject: [PATCH 10/65] Cleanup. --- .../TripleSlashSyntaxRewriter.cs | 36 +++++++++++-------- Libraries/ToTripleSlashPorter.cs | 1 - 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index b86008d..8dfd20d 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -131,7 +131,7 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod if (!UseBoilerplate) { - if (!TryGetType(node, symbol, out DocsType? type)) + if (!TryGetType(symbol, out DocsType? type)) { return node; } @@ -239,13 +239,6 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(xmlRemarksContent); return GetXmlTrivia(xmlRemarks, leadingWhitespace); - - //DocumentationCommentTriviaSyntax triviaNode = SyntaxFactory.DocumentationComment(SyntaxKind.SingleLineDocumentationCommentTrivia, content); - //SyntaxTrivia docCommentTrivia = SyntaxFactory.Trivia(triviaNode); - - //return leadingWhitespace - //.Add(docCommentTrivia) - //.Add(SyntaxFactory.CarriageReturnLineFeed); } return new(); @@ -267,7 +260,7 @@ private SyntaxTriviaList GetParam(string name, string text, SyntaxTriviaList lea private SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace) { - var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute("name", text)); + var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute(name, text)); SyntaxList contents = GetContentsInRows(text); return GetXmlTrivia("typeparam", attribute, contents, leadingWhitespace); } @@ -333,19 +326,32 @@ private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitesp private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace) { string whitespace = leadingWhitespace.ToFullString().Replace(Environment.NewLine, ""); + SyntaxToken newLineAndWhitespace = SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace); + + SyntaxTrivia leadingTrivia = SyntaxFactory.SyntaxTrivia(SyntaxKind.DocumentationCommentExteriorTrivia, string.Empty); + SyntaxTriviaList leading = SyntaxTriviaList.Create(leadingTrivia); + var tokens = new List(); - foreach (string line in text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)) + tokens.Add(newLineAndWhitespace); + foreach (string line in text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { - tokens.Add(SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace)); // Needs to be textnewline, and below needs to be textliteral - tokens.Add(SyntaxFactory.XmlTextLiteral(SyntaxTriviaList.Create(SyntaxFactory.SyntaxTrivia(SyntaxKind.DocumentationCommentExteriorTrivia, string.Empty)), line, line, default)); + SyntaxToken token = SyntaxFactory.XmlTextLiteral(leading, line, line, default); + tokens.Add(token); + tokens.Add(newLineAndWhitespace); } - return SyntaxFactory.TokenList(tokens); } private SyntaxList GetContentsInRows(string text) { - return new(SyntaxFactory.XmlText(text)); // TODO: Press enter! + var nodes = new SyntaxList(); + foreach (string line in text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var tokenList = SyntaxFactory.ParseTokens(line).ToArray(); // Prevents unexpected change from "<" to "<" + XmlTextSyntax xmlText = SyntaxFactory.XmlText(tokenList); + return nodes.Add(xmlText); + } + return nodes; } private SyntaxTriviaList GetXmlTrivia(XmlNodeSyntax node, SyntaxTriviaList leadingWhitespace) @@ -393,7 +399,7 @@ private bool TryGetMember(SyntaxNode node, [NotNullWhen(returnValue: true)] out return member != null; } - private bool TryGetType(SyntaxNode node, ISymbol symbol, [NotNullWhen(returnValue: true)] out DocsType? type) + private bool TryGetType(ISymbol symbol, [NotNullWhen(returnValue: true)] out DocsType? type) { type = null; diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index 382c84b..242764c 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -5,7 +5,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.MSBuild; using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; From ec2b4b4a06dd5400cd1954da6109d3db37e45226 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 5 Jan 2021 17:39:15 -0800 Subject: [PATCH 11/65] Newline fine tuning. --- .../TripleSlashSyntaxRewriter.cs | 44 ++++++++++++++++--- Libraries/ToTripleSlashPorter.cs | 8 ++-- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index 8dfd20d..8432ee4 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -25,6 +25,11 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod UseBoilerplate = useBoilerplate; } + /// + /// + /// + /// + /// public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) { SyntaxNode? baseNode = base.VisitClassDeclaration(node); @@ -233,8 +238,8 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp { if (!UseBoilerplate && !text.IsDocsEmpty()) { - string trimmedRemarks = text.RemoveSubstrings("").Trim(); - SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed)); + string trimmedRemarks = text.RemoveSubstrings("").Trim(); // The SyntaxFactory needs to add this + SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true); XmlNodeSyntax xmlRemarksContent = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(xmlRemarksContent); @@ -296,7 +301,7 @@ private SyntaxTriviaList GetExceptions(DocsMember member, SyntaxTriviaList leadi private SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace) { TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); - XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text, leadingWhitespace)); + XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text, leadingWhitespace, addInitialNewLine: false)); XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); return GetXmlTrivia(element, leadingWhitespace); } @@ -323,7 +328,7 @@ private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitesp return GetXmlTrivia(element, leadingWhitespace); } - private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace) + private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace, bool addInitialNewLine) { string whitespace = leadingWhitespace.ToFullString().Replace(Environment.NewLine, ""); SyntaxToken newLineAndWhitespace = SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace); @@ -332,12 +337,37 @@ private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhi SyntaxTriviaList leading = SyntaxTriviaList.Create(leadingTrivia); var tokens = new List(); - tokens.Add(newLineAndWhitespace); - foreach (string line in text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + + string[] splittedLines = text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + // Only add the initial new line and whitespace if the contents have more than one line. Otherwise, we want the contents to be inlined inside the tags. + if (splittedLines.Length > 1 && addInitialNewLine) + { + // For example, the remarks section needs a new line before the initial "## Remarks" title + tokens.Add(newLineAndWhitespace); + tokens.Add(newLineAndWhitespace); + } + + int lineNumber = 1; + foreach (string line in splittedLines) { SyntaxToken token = SyntaxFactory.XmlTextLiteral(leading, line, line, default); tokens.Add(token); - tokens.Add(newLineAndWhitespace); + + // Only add extra new lines if we expect more than one line of text in the contents. Otherwise, inline it inside the tags. + if (splittedLines.Length > 1) + { + tokens.Add(newLineAndWhitespace); + + if (lineNumber < splittedLines.Length) + { + // New line characters between sentences need to have their own separate line + // but need to avoid adding a final single separate line + tokens.Add(newLineAndWhitespace); + } + } + + lineNumber++; } return SyntaxFactory.TokenList(tokens); } diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index 242764c..dfabe2c 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -82,10 +82,10 @@ public void Start() Log.ErrorAndExit("Exiting due to diagnostic errors found."); } - PortCommentsForAPIs(compilation!); + PortCommentsForProject(compilation!); } - private void PortCommentsForAPIs(Compilation compilation) + private void PortCommentsForProject(Compilation compilation) { foreach (DocsType docsType in DocsComments.Types) { @@ -99,11 +99,11 @@ private void PortCommentsForAPIs(Compilation compilation) continue; } - PortAPI(compilation, docsType, typeSymbol); + PortCommentsForType(compilation, docsType, typeSymbol); } } - private void PortAPI(Compilation compilation, IDocsAPI api, ISymbol symbol) + private void PortCommentsForType(Compilation compilation, IDocsAPI api, ISymbol symbol) { bool useBoilerplate = false; foreach (Location location in symbol.Locations) From 5241e52fcff9e55ce86d4fabda173d937764354b Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 5 Jan 2021 17:39:38 -0800 Subject: [PATCH 12/65] Move existing unit test code to PortToDocs folder. --- Tests/{Tests.cs => PortToDocs/PortToDocsTests.cs} | 2 +- Tests/{ => PortToDocs}/TestData.cs | 2 +- .../TestData/AssemblyAndNamespaceDifferent/DocsExpected.xml | 0 .../TestData/AssemblyAndNamespaceDifferent/DocsOriginal.xml | 0 .../TestData/AssemblyAndNamespaceDifferent/TSOriginal.xml | 0 .../TestData/AssemblyAndNamespaceSame/DocsExpected.xml | 0 .../TestData/AssemblyAndNamespaceSame/DocsOriginal.xml | 0 .../TestData/AssemblyAndNamespaceSame/TSOriginal.xml | 0 Tests/{ => PortToDocs}/TestData/Basic/DocsExpected.xml | 0 Tests/{ => PortToDocs}/TestData/Basic/DocsOriginal.xml | 0 Tests/{ => PortToDocs}/TestData/Basic/TSOriginal.xml | 0 .../TestData/DontAddMissingRemarks/DocsExpected.xml | 0 .../TestData/DontAddMissingRemarks/DocsOriginal.xml | 0 .../TestData/DontAddMissingRemarks/TSOriginal.xml | 0 .../TestData/Exception_ExistingCref/DocsExpected.xml | 0 .../TestData/Exception_ExistingCref/DocsOriginal.xml | 0 .../TestData/Exception_ExistingCref/TSOriginal.xml | 0 Tests/{ => PortToDocs}/TestData/Exceptions/DocsExpected.xml | 0 Tests/{ => PortToDocs}/TestData/Exceptions/DocsOriginal.xml | 0 Tests/{ => PortToDocs}/TestData/Exceptions/TSOriginal.xml | 0 .../TestData/Remarks_NoEII_NoInterfaceRemarks/DocsExpected.xml | 0 .../TestData/Remarks_NoEII_NoInterfaceRemarks/DocsInterface.xml | 0 .../TestData/Remarks_NoEII_NoInterfaceRemarks/DocsOriginal.xml | 0 .../TestData/Remarks_NoEII_NoInterfaceRemarks/TSOriginal.xml | 0 .../Remarks_WithEII_NoInterfaceRemarks/DocsExpected.xml | 0 .../Remarks_WithEII_NoInterfaceRemarks/DocsInterface.xml | 0 .../Remarks_WithEII_NoInterfaceRemarks/DocsOriginal.xml | 0 .../TestData/Remarks_WithEII_NoInterfaceRemarks/TSOriginal.xml | 0 .../Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml | 0 .../Remarks_WithEII_WithInterfaceRemarks/DocsInterface.xml | 0 .../Remarks_WithEII_WithInterfaceRemarks/DocsOriginal.xml | 0 .../Remarks_WithEII_WithInterfaceRemarks/TSOriginal.xml | 0 32 files changed, 2 insertions(+), 2 deletions(-) rename Tests/{Tests.cs => PortToDocs/PortToDocsTests.cs} (99%) rename Tests/{ => PortToDocs}/TestData.cs (97%) rename Tests/{ => PortToDocs}/TestData/AssemblyAndNamespaceDifferent/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/AssemblyAndNamespaceDifferent/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/AssemblyAndNamespaceDifferent/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/AssemblyAndNamespaceSame/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/AssemblyAndNamespaceSame/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/AssemblyAndNamespaceSame/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Basic/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/Basic/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Basic/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/DontAddMissingRemarks/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/DontAddMissingRemarks/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/DontAddMissingRemarks/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Exception_ExistingCref/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/Exception_ExistingCref/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Exception_ExistingCref/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Exceptions/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/Exceptions/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Exceptions/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsInterface.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_NoEII_NoInterfaceRemarks/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsInterface.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_NoInterfaceRemarks/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsInterface.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_WithInterfaceRemarks/TSOriginal.xml (100%) diff --git a/Tests/Tests.cs b/Tests/PortToDocs/PortToDocsTests.cs similarity index 99% rename from Tests/Tests.cs rename to Tests/PortToDocs/PortToDocsTests.cs index 8f8c6fc..7e29411 100644 --- a/Tests/Tests.cs +++ b/Tests/PortToDocs/PortToDocsTests.cs @@ -3,7 +3,7 @@ namespace Libraries.Tests { - public class Tests + public class PortToDocsTests { [Fact] // Verifies the basic case of porting all regular fields. diff --git a/Tests/TestData.cs b/Tests/PortToDocs/TestData.cs similarity index 97% rename from Tests/TestData.cs rename to Tests/PortToDocs/TestData.cs index 846feef..391d7de 100644 --- a/Tests/TestData.cs +++ b/Tests/PortToDocs/TestData.cs @@ -5,7 +5,7 @@ namespace Libraries.Tests { public class TestData { - private string TestDataRootDir => @"..\..\..\TestData"; + private string TestDataRootDir => @"..\..\..\PortToDocs\TestData"; public const string TestAssembly = "MyAssembly"; public const string TestNamespace = "MyNamespace"; diff --git a/Tests/TestData/AssemblyAndNamespaceDifferent/DocsExpected.xml b/Tests/PortToDocs/TestData/AssemblyAndNamespaceDifferent/DocsExpected.xml similarity index 100% rename from Tests/TestData/AssemblyAndNamespaceDifferent/DocsExpected.xml rename to Tests/PortToDocs/TestData/AssemblyAndNamespaceDifferent/DocsExpected.xml diff --git a/Tests/TestData/AssemblyAndNamespaceDifferent/DocsOriginal.xml b/Tests/PortToDocs/TestData/AssemblyAndNamespaceDifferent/DocsOriginal.xml similarity index 100% rename from Tests/TestData/AssemblyAndNamespaceDifferent/DocsOriginal.xml rename to Tests/PortToDocs/TestData/AssemblyAndNamespaceDifferent/DocsOriginal.xml diff --git a/Tests/TestData/AssemblyAndNamespaceDifferent/TSOriginal.xml b/Tests/PortToDocs/TestData/AssemblyAndNamespaceDifferent/TSOriginal.xml similarity index 100% rename from Tests/TestData/AssemblyAndNamespaceDifferent/TSOriginal.xml rename to Tests/PortToDocs/TestData/AssemblyAndNamespaceDifferent/TSOriginal.xml diff --git a/Tests/TestData/AssemblyAndNamespaceSame/DocsExpected.xml b/Tests/PortToDocs/TestData/AssemblyAndNamespaceSame/DocsExpected.xml similarity index 100% rename from Tests/TestData/AssemblyAndNamespaceSame/DocsExpected.xml rename to Tests/PortToDocs/TestData/AssemblyAndNamespaceSame/DocsExpected.xml diff --git a/Tests/TestData/AssemblyAndNamespaceSame/DocsOriginal.xml b/Tests/PortToDocs/TestData/AssemblyAndNamespaceSame/DocsOriginal.xml similarity index 100% rename from Tests/TestData/AssemblyAndNamespaceSame/DocsOriginal.xml rename to Tests/PortToDocs/TestData/AssemblyAndNamespaceSame/DocsOriginal.xml diff --git a/Tests/TestData/AssemblyAndNamespaceSame/TSOriginal.xml b/Tests/PortToDocs/TestData/AssemblyAndNamespaceSame/TSOriginal.xml similarity index 100% rename from Tests/TestData/AssemblyAndNamespaceSame/TSOriginal.xml rename to Tests/PortToDocs/TestData/AssemblyAndNamespaceSame/TSOriginal.xml diff --git a/Tests/TestData/Basic/DocsExpected.xml b/Tests/PortToDocs/TestData/Basic/DocsExpected.xml similarity index 100% rename from Tests/TestData/Basic/DocsExpected.xml rename to Tests/PortToDocs/TestData/Basic/DocsExpected.xml diff --git a/Tests/TestData/Basic/DocsOriginal.xml b/Tests/PortToDocs/TestData/Basic/DocsOriginal.xml similarity index 100% rename from Tests/TestData/Basic/DocsOriginal.xml rename to Tests/PortToDocs/TestData/Basic/DocsOriginal.xml diff --git a/Tests/TestData/Basic/TSOriginal.xml b/Tests/PortToDocs/TestData/Basic/TSOriginal.xml similarity index 100% rename from Tests/TestData/Basic/TSOriginal.xml rename to Tests/PortToDocs/TestData/Basic/TSOriginal.xml diff --git a/Tests/TestData/DontAddMissingRemarks/DocsExpected.xml b/Tests/PortToDocs/TestData/DontAddMissingRemarks/DocsExpected.xml similarity index 100% rename from Tests/TestData/DontAddMissingRemarks/DocsExpected.xml rename to Tests/PortToDocs/TestData/DontAddMissingRemarks/DocsExpected.xml diff --git a/Tests/TestData/DontAddMissingRemarks/DocsOriginal.xml b/Tests/PortToDocs/TestData/DontAddMissingRemarks/DocsOriginal.xml similarity index 100% rename from Tests/TestData/DontAddMissingRemarks/DocsOriginal.xml rename to Tests/PortToDocs/TestData/DontAddMissingRemarks/DocsOriginal.xml diff --git a/Tests/TestData/DontAddMissingRemarks/TSOriginal.xml b/Tests/PortToDocs/TestData/DontAddMissingRemarks/TSOriginal.xml similarity index 100% rename from Tests/TestData/DontAddMissingRemarks/TSOriginal.xml rename to Tests/PortToDocs/TestData/DontAddMissingRemarks/TSOriginal.xml diff --git a/Tests/TestData/Exception_ExistingCref/DocsExpected.xml b/Tests/PortToDocs/TestData/Exception_ExistingCref/DocsExpected.xml similarity index 100% rename from Tests/TestData/Exception_ExistingCref/DocsExpected.xml rename to Tests/PortToDocs/TestData/Exception_ExistingCref/DocsExpected.xml diff --git a/Tests/TestData/Exception_ExistingCref/DocsOriginal.xml b/Tests/PortToDocs/TestData/Exception_ExistingCref/DocsOriginal.xml similarity index 100% rename from Tests/TestData/Exception_ExistingCref/DocsOriginal.xml rename to Tests/PortToDocs/TestData/Exception_ExistingCref/DocsOriginal.xml diff --git a/Tests/TestData/Exception_ExistingCref/TSOriginal.xml b/Tests/PortToDocs/TestData/Exception_ExistingCref/TSOriginal.xml similarity index 100% rename from Tests/TestData/Exception_ExistingCref/TSOriginal.xml rename to Tests/PortToDocs/TestData/Exception_ExistingCref/TSOriginal.xml diff --git a/Tests/TestData/Exceptions/DocsExpected.xml b/Tests/PortToDocs/TestData/Exceptions/DocsExpected.xml similarity index 100% rename from Tests/TestData/Exceptions/DocsExpected.xml rename to Tests/PortToDocs/TestData/Exceptions/DocsExpected.xml diff --git a/Tests/TestData/Exceptions/DocsOriginal.xml b/Tests/PortToDocs/TestData/Exceptions/DocsOriginal.xml similarity index 100% rename from Tests/TestData/Exceptions/DocsOriginal.xml rename to Tests/PortToDocs/TestData/Exceptions/DocsOriginal.xml diff --git a/Tests/TestData/Exceptions/TSOriginal.xml b/Tests/PortToDocs/TestData/Exceptions/TSOriginal.xml similarity index 100% rename from Tests/TestData/Exceptions/TSOriginal.xml rename to Tests/PortToDocs/TestData/Exceptions/TSOriginal.xml diff --git a/Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsExpected.xml b/Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsExpected.xml similarity index 100% rename from Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsExpected.xml rename to Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsExpected.xml diff --git a/Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsInterface.xml b/Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsInterface.xml similarity index 100% rename from Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsInterface.xml rename to Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsInterface.xml diff --git a/Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsOriginal.xml b/Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsOriginal.xml similarity index 100% rename from Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsOriginal.xml rename to Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsOriginal.xml diff --git a/Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/TSOriginal.xml b/Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/TSOriginal.xml similarity index 100% rename from Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/TSOriginal.xml rename to Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/TSOriginal.xml diff --git a/Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsExpected.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsExpected.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsExpected.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsExpected.xml diff --git a/Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsInterface.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsInterface.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsInterface.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsInterface.xml diff --git a/Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsOriginal.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsOriginal.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsOriginal.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsOriginal.xml diff --git a/Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/TSOriginal.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/TSOriginal.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/TSOriginal.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/TSOriginal.xml diff --git a/Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml diff --git a/Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsInterface.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsInterface.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsInterface.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsInterface.xml diff --git a/Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsOriginal.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsOriginal.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsOriginal.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsOriginal.xml diff --git a/Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/TSOriginal.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/TSOriginal.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/TSOriginal.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/TSOriginal.xml From d511989bc6925ece8201efb4da28476b3cb49423 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 6 Jan 2021 16:19:05 -0800 Subject: [PATCH 13/65] Move MSBuild registering code into its own method and call it from constructor. --- Libraries/Configuration.cs | 4 +- Libraries/ToTripleSlashPorter.cs | 102 +++++++++++++++++++++++++++++++ Program/DocsPortingTool.cs | 98 ----------------------------- 3 files changed, 104 insertions(+), 100 deletions(-) diff --git a/Libraries/Configuration.cs b/Libraries/Configuration.cs index b39af89..1af406e 100644 --- a/Libraries/Configuration.cs +++ b/Libraries/Configuration.cs @@ -57,8 +57,8 @@ private enum Mode public readonly string BinLogPath = "output.binlog"; public bool BinLogger { get; private set; } = false; - public FileInfo? CsProj { get; private set; } - public PortingDirection Direction { get; private set; } = PortingDirection.ToDocs; + public FileInfo? CsProj { get; set; } + public PortingDirection Direction { get; set; } = PortingDirection.ToDocs; public List DirsIntelliSense { get; } = new List(); public List DirsDocsXml { get; } = new List(); public bool DisablePrompts { get; set; } = false; diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index dfabe2c..1b7e176 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -9,6 +9,10 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Linq; +using Microsoft.Build.Locator; +using System.Collections.Generic; +using System.Runtime.Loader; namespace Libraries { @@ -16,6 +20,7 @@ public class ToTripleSlashPorter { private readonly Configuration Config; private readonly DocsCommentsContainer DocsComments; + private VisualStudioInstance MSBuildInstance; public ToTripleSlashPorter(Configuration config) { @@ -25,6 +30,11 @@ public ToTripleSlashPorter(Configuration config) } Config = config; DocsComments = new DocsCommentsContainer(config); + + // This ensures we can load MSBuild property before calling the ToTripleSlashPorter constructor + MSBuildInstance = MSBuildLocator.QueryVisualStudioInstances().First(); + Register(MSBuildInstance.MSBuildPath); + MSBuildLocator.RegisterInstance(MSBuildInstance); } public void Start() @@ -128,5 +138,97 @@ private void PortCommentsForType(Compilation compilation, IDocsAPI api, ISymbol useBoilerplate = true; } } + + #region MSBuild loading logic + + private static readonly Dictionary s_pathsToAssemblies = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary s_namesToAssemblies = new Dictionary(); + + private static readonly object s_guard = new object(); + + /// + /// Register an assembly loader that will load assemblies with higher version than what was requested. + /// + private static void Register(string searchPath) + { + AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext context, AssemblyName assemblyName) => + { + lock (s_guard) + { + if (s_namesToAssemblies.TryGetValue(assemblyName.FullName, out var cachedAssembly)) + { + return cachedAssembly; + } + + var assembly = TryResolveAssemblyFromPaths(context, assemblyName, searchPath, s_pathsToAssemblies); + + // Cache assembly + if (assembly != null) + { + var name = assembly.FullName; + if (name is null) + { + throw new Exception($"Could not get name for assembly '{assembly}'"); + } + + s_pathsToAssemblies[assembly.Location] = assembly; + s_namesToAssemblies[name] = assembly; + } + + return assembly; + } + }; + } + private static Assembly? TryResolveAssemblyFromPaths(AssemblyLoadContext context, AssemblyName assemblyName, string searchPath, Dictionary? knownAssemblyPaths = null) + { + foreach (var cultureSubfolder in string.IsNullOrEmpty(assemblyName.CultureName) + // If no culture is specified, attempt to load directly from + // the known dependency paths. + ? new[] { string.Empty } + // Search for satellite assemblies in culture subdirectories + // of the assembly search directories, but fall back to the + // bare search directory if that fails. + : new[] { assemblyName.CultureName, string.Empty }) + { + foreach (var extension in new[] { "ni.dll", "ni.exe", "dll", "exe" }) + { + var candidatePath = Path.Combine( + searchPath, cultureSubfolder, $"{assemblyName.Name}.{extension}"); + + var isAssemblyLoaded = knownAssemblyPaths?.ContainsKey(candidatePath) == true; + if (isAssemblyLoaded || !File.Exists(candidatePath)) + { + continue; + } + + var candidateAssemblyName = AssemblyLoadContext.GetAssemblyName(candidatePath); + if (candidateAssemblyName.Version < assemblyName.Version) + { + continue; + } + + try + { + var assembly = context.LoadFromAssemblyPath(candidatePath); + return assembly; + } + catch + { + if (assemblyName.Name != null) + { + // We were unable to load the assembly from the file path. It is likely that + // a different version of the assembly has already been loaded into the context. + // Be forgiving and attempt to load assembly by name without specifying a version. + return context.LoadFromAssemblyName(new AssemblyName(assemblyName.Name)); + } + } + } + } + + return null; + } + + #endregion + } } diff --git a/Program/DocsPortingTool.cs b/Program/DocsPortingTool.cs index 92d1c70..7aae3f8 100644 --- a/Program/DocsPortingTool.cs +++ b/Program/DocsPortingTool.cs @@ -1,12 +1,6 @@ #nullable enable using Libraries; using System; -using System.Collections.Generic; -using System.Reflection; -using System.Runtime.Loader; -using System.IO; -using System.Linq; -using Microsoft.Build.Locator; namespace DocsPortingTool { @@ -25,11 +19,6 @@ public static void Main(string[] args) } case Configuration.PortingDirection.ToTripleSlash: { - // This ensures we can load MSBuild property before calling the ToTripleSlashPorter constructor - VisualStudioInstance? msBuildInstance = MSBuildLocator.QueryVisualStudioInstances().First(); - Register(msBuildInstance.MSBuildPath); - MSBuildLocator.RegisterInstance(msBuildInstance); - var porter = new ToTripleSlashPorter(config); porter.Start(); break; @@ -38,92 +27,5 @@ public static void Main(string[] args) throw new ArgumentOutOfRangeException($"Unrecognized porting direction: {config.Direction}"); } } - - private static readonly Dictionary s_pathsToAssemblies = new Dictionary(StringComparer.OrdinalIgnoreCase); - private static readonly Dictionary s_namesToAssemblies = new Dictionary(); - - private static readonly object s_guard = new object(); - - /// - /// Register an assembly loader that will load assemblies with higher version than what was requested. - /// - private static void Register(string searchPath) - { - AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext context, AssemblyName assemblyName) => - { - lock (s_guard) - { - if (s_namesToAssemblies.TryGetValue(assemblyName.FullName, out var cachedAssembly)) - { - return cachedAssembly; - } - - var assembly = TryResolveAssemblyFromPaths(context, assemblyName, searchPath, s_pathsToAssemblies); - - // Cache assembly - if (assembly != null) - { - var name = assembly.FullName; - if (name is null) - { - throw new Exception($"Could not get name for assembly '{assembly}'"); - } - - s_pathsToAssemblies[assembly.Location] = assembly; - s_namesToAssemblies[name] = assembly; - } - - return assembly; - } - }; - } - internal static Assembly? TryResolveAssemblyFromPaths(AssemblyLoadContext context, AssemblyName assemblyName, string searchPath, Dictionary? knownAssemblyPaths = null) - { - foreach (var cultureSubfolder in string.IsNullOrEmpty(assemblyName.CultureName) - // If no culture is specified, attempt to load directly from - // the known dependency paths. - ? new[] { string.Empty } - // Search for satellite assemblies in culture subdirectories - // of the assembly search directories, but fall back to the - // bare search directory if that fails. - : new[] { assemblyName.CultureName, string.Empty }) - { - foreach (var extension in new[] { "ni.dll", "ni.exe", "dll", "exe" }) - { - var candidatePath = Path.Combine( - searchPath, cultureSubfolder, $"{assemblyName.Name}.{extension}"); - - var isAssemblyLoaded = knownAssemblyPaths?.ContainsKey(candidatePath) == true; - if (isAssemblyLoaded || !File.Exists(candidatePath)) - { - continue; - } - - var candidateAssemblyName = AssemblyLoadContext.GetAssemblyName(candidatePath); - if (candidateAssemblyName.Version < assemblyName.Version) - { - continue; - } - - try - { - var assembly = context.LoadFromAssemblyPath(candidatePath); - return assembly; - } - catch - { - if (assemblyName.Name != null) - { - // We were unable to load the assembly from the file path. It is likely that - // a different version of the assembly has already been loaded into the context. - // Be forgiving and attempt to load assembly by name without specifying a version. - return context.LoadFromAssemblyName(new AssemblyName(assemblyName.Name)); - } - } - } - } - - return null; - } } } From d101c51acb344964c6b0cd02cf6ff8e7d08e52c4 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 6 Jan 2021 16:19:24 -0800 Subject: [PATCH 14/65] Add tests for docs to triple slash porting. --- Tests/PortToDocs/PortToDocsTestData.cs | 73 +++++++++ Tests/PortToDocs/PortToDocsTests.cs | 21 +-- Tests/PortToDocs/TestData.cs | 80 ---------- .../PortToTripleSlashTestData.cs | 67 ++++++++ .../PortToTripleSlashTests.cs | 68 ++++++++ .../TestData/Basic/DocsOriginal.xml | 151 ++++++++++++++++++ .../TestData/Basic/Project.csproj | 9 ++ .../TestData/Basic/SourceExpected.cs | 87 ++++++++++ .../TestData/Basic/SourceOriginal.cs | 34 ++++ Tests/TestData.cs | 22 +++ Tests/Tests.csproj | 14 +- 11 files changed, 535 insertions(+), 91 deletions(-) create mode 100644 Tests/PortToDocs/PortToDocsTestData.cs delete mode 100644 Tests/PortToDocs/TestData.cs create mode 100644 Tests/PortToTripleSlash/PortToTripleSlashTestData.cs create mode 100644 Tests/PortToTripleSlash/PortToTripleSlashTests.cs create mode 100644 Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml create mode 100644 Tests/PortToTripleSlash/TestData/Basic/Project.csproj create mode 100644 Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs create mode 100644 Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs create mode 100644 Tests/TestData.cs diff --git a/Tests/PortToDocs/PortToDocsTestData.cs b/Tests/PortToDocs/PortToDocsTestData.cs new file mode 100644 index 0000000..5fc16c8 --- /dev/null +++ b/Tests/PortToDocs/PortToDocsTestData.cs @@ -0,0 +1,73 @@ +using System.IO; +using Xunit; + +namespace Libraries.Tests +{ + internal class PortToDocsTestData : TestData + { + private const string TestDataRootDirPath = @"../../../PortToDocs/TestData"; + private const string IntellisenseAndDllDirName = "IntelliSenseAndDLL"; + + internal DirectoryInfo IntelliSenseAndDLLDir { get; set; } + + // Docs file with the interface from which the type inherits. + internal string InterfaceFilePath { get; set; } + + internal PortToDocsTestData( + TestDirectory tempDir, + string testDataDir, + string assemblyName, + string namespaceName, + string typeName, + bool skipInterfaceImplementations = true) + { + Assert.False(string.IsNullOrWhiteSpace(assemblyName)); + Assert.False(string.IsNullOrWhiteSpace(typeName)); + + Assembly = assemblyName; + Namespace = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; + Type = typeName; + + IntelliSenseAndDLLDir = tempDir.CreateSubdirectory(IntellisenseAndDllDirName); + DirectoryInfo tripleSlashAssemblyDir = IntelliSenseAndDLLDir.CreateSubdirectory(Assembly); + + DocsDir = tempDir.CreateSubdirectory(DocsDirName); + DirectoryInfo docsAssemblyDir = DocsDir.CreateSubdirectory(Namespace); + + string testDataPath = Path.Combine(TestDataRootDirPath, testDataDir); + + string tripleSlashOriginalFilePath = Path.Combine(testDataPath, "TSOriginal.xml"); + string docsOriginalFilePath = Path.Combine(testDataPath, "DocsOriginal.xml"); + string docsExpectedFilePath = Path.Combine(testDataPath, "DocsExpected.xml"); + + Assert.True(File.Exists(tripleSlashOriginalFilePath)); + Assert.True(File.Exists(docsOriginalFilePath)); + Assert.True(File.Exists(docsExpectedFilePath)); + + OriginalFilePath = Path.Combine(tripleSlashAssemblyDir.FullName, $"{Type}.xml"); + ActualFilePath = Path.Combine(docsAssemblyDir.FullName, $"{Type}.xml"); + ExpectedFilePath = Path.Combine(tempDir.FullPath, "DocsExpected.xml"); + + File.Copy(tripleSlashOriginalFilePath, OriginalFilePath); + File.Copy(docsOriginalFilePath, ActualFilePath); + File.Copy(docsExpectedFilePath, ExpectedFilePath); + + Assert.True(File.Exists(OriginalFilePath)); + Assert.True(File.Exists(ActualFilePath)); + Assert.True(File.Exists(ExpectedFilePath)); + + if (!skipInterfaceImplementations) + { + string interfaceFilePath = Path.Combine(testDataPath, "DocsInterface.xml"); + Assert.True(File.Exists(interfaceFilePath)); + + string interfaceAssembly = "System"; + DirectoryInfo interfaceAssemblyDir = DocsDir.CreateSubdirectory(interfaceAssembly); + InterfaceFilePath = Path.Combine(interfaceAssemblyDir.FullName, "IMyInterface.xml"); + File.Copy(interfaceFilePath, InterfaceFilePath); + Assert.True(File.Exists(InterfaceFilePath)); + } + } + } + +} diff --git a/Tests/PortToDocs/PortToDocsTests.cs b/Tests/PortToDocs/PortToDocsTests.cs index 7e29411..3343128 100644 --- a/Tests/PortToDocs/PortToDocsTests.cs +++ b/Tests/PortToDocs/PortToDocsTests.cs @@ -101,7 +101,7 @@ private void PortToDocs( { using TestDirectory tempDir = new TestDirectory(); - TestData testData = new TestData( + PortToDocsTestData testData = new PortToDocsTestData( tempDir, testDataDir, skipInterfaceImplementations: skipInterfaceImplementations, @@ -110,17 +110,18 @@ private void PortToDocs( typeName: typeName ); - Configuration c = new Configuration + Configuration c = new() { + Direction = Configuration.PortingDirection.ToDocs, DisablePrompts = disablePrompts, + ExceptionCollisionThreshold = exceptionCollisionThreshold, + PortExceptionsExisting = portExceptionsExisting, + PortMemberRemarks = portMemberRemarks, + PortTypeRemarks = portTypeRemarks, PrintUndoc = printUndoc, Save = save, SkipInterfaceImplementations = skipInterfaceImplementations, - SkipInterfaceRemarks = skipInterfaceRemarks, - PortTypeRemarks = portTypeRemarks, - PortMemberRemarks = portMemberRemarks, - PortExceptionsExisting = portExceptionsExisting, - ExceptionCollisionThreshold = exceptionCollisionThreshold + SkipInterfaceRemarks = skipInterfaceRemarks }; c.IncludedAssemblies.Add(assemblyName); @@ -130,8 +131,8 @@ private void PortToDocs( c.IncludedNamespaces.Add(namespaceName); } - c.DirsDocsXml.Add(testData.Docs); - c.DirsIntelliSense.Add(testData.IntelliSenseAndDLL); + c.DirsDocsXml.Add(testData.DocsDir); + c.DirsIntelliSense.Add(testData.IntelliSenseAndDLLDir); var porter = new ToDocsPorter(c); porter.Start(); @@ -139,7 +140,7 @@ private void PortToDocs( Verify(testData); } - private void Verify(TestData testData) + private void Verify(PortToDocsTestData testData) { string[] expectedLines = File.ReadAllLines(testData.ExpectedFilePath); string[] actualLines = File.ReadAllLines(testData.ActualFilePath); diff --git a/Tests/PortToDocs/TestData.cs b/Tests/PortToDocs/TestData.cs deleted file mode 100644 index 391d7de..0000000 --- a/Tests/PortToDocs/TestData.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.IO; -using Xunit; - -namespace Libraries.Tests -{ - public class TestData - { - private string TestDataRootDir => @"..\..\..\PortToDocs\TestData"; - - public const string TestAssembly = "MyAssembly"; - public const string TestNamespace = "MyNamespace"; - public const string TestType = "MyType"; - - public string Assembly { get; private set; } - public string Namespace { get; private set; } - public string Type { get; private set; } - public DirectoryInfo IntelliSenseAndDLL { get; private set; } - public DirectoryInfo Docs { get; private set; } - - /// IntelliSense xml file. - public string OriginalFilePath { get; private set; } - /// Docs file as we should expect it to look. - public string ExpectedFilePath { get; private set; } - /// Docs file the tool will modify. - public string ActualFilePath { get; private set; } - /// Docs file with the interface from which the type inherits. - public string InterfaceFilePath { get; private set; } - - public TestData(TestDirectory tempDir, string testDataDir, string assemblyName, string namespaceName, string typeName, bool skipInterfaceImplementations = true) - { - Assert.False(string.IsNullOrWhiteSpace(assemblyName)); - Assert.False(string.IsNullOrWhiteSpace(typeName)); - - Assembly = assemblyName; - Namespace = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; - Type = typeName; - - IntelliSenseAndDLL = tempDir.CreateSubdirectory("IntelliSenseAndDLL"); - DirectoryInfo tsAssemblyDir = IntelliSenseAndDLL.CreateSubdirectory(Assembly); - - Docs = tempDir.CreateSubdirectory("Docs"); - DirectoryInfo docsAssemblyDir = Docs.CreateSubdirectory(Namespace); - - string testDataPath = Path.Combine(TestDataRootDir, testDataDir); - - string tsOriginFilePath = Path.Combine(testDataPath, "TSOriginal.xml"); - string docsOriginFilePath = Path.Combine(testDataPath, "DocsOriginal.xml"); - string docsOriginExpectedFilePath = Path.Combine(testDataPath, "DocsExpected.xml"); - - Assert.True(File.Exists(tsOriginFilePath)); - Assert.True(File.Exists(docsOriginFilePath)); - Assert.True(File.Exists(docsOriginExpectedFilePath)); - - OriginalFilePath = Path.Combine(tsAssemblyDir.FullName, $"{Type}.xml"); - ActualFilePath = Path.Combine(docsAssemblyDir.FullName, $"{Type}.xml"); - ExpectedFilePath = Path.Combine(tempDir.FullPath, "DocsExpected.xml"); - - File.Copy(tsOriginFilePath, OriginalFilePath); - File.Copy(docsOriginFilePath, ActualFilePath); - File.Copy(docsOriginExpectedFilePath, ExpectedFilePath); - - Assert.True(File.Exists(OriginalFilePath)); - Assert.True(File.Exists(ActualFilePath)); - Assert.True(File.Exists(ExpectedFilePath)); - - if (!skipInterfaceImplementations) - { - string interfaceFilePath = Path.Combine(testDataPath, "DocsInterface.xml"); - Assert.True(File.Exists(interfaceFilePath)); - - string interfaceAssembly = "System"; - DirectoryInfo interfaceAssemblyDir = Docs.CreateSubdirectory(interfaceAssembly); - InterfaceFilePath = Path.Combine(interfaceAssemblyDir.FullName, "IMyInterface.xml"); - File.Copy(interfaceFilePath, InterfaceFilePath); - Assert.True(File.Exists(InterfaceFilePath)); - } - } - } - -} diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs b/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs new file mode 100644 index 0000000..4d3c004 --- /dev/null +++ b/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Libraries.Tests +{ + internal class PortToTripleSlashTestData : TestData + { + private string TestDataRootDirPath => @"../../../PortToTripleSlash/TestData"; + private const string ProjectDirName = "Project"; + private const string ProjectFileName = "Project.csproj"; + private DirectoryInfo ProjectDir { get; set; } + + internal string ProjectFilePath { get; set; } + + internal PortToTripleSlashTestData( + TestDirectory tempDir, + string testDataDir, + string assemblyName, + string namespaceName, + string typeName) + { + Assert.False(string.IsNullOrWhiteSpace(assemblyName)); + Assert.False(string.IsNullOrWhiteSpace(typeName)); + + Assembly = assemblyName; + Namespace = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; + Type = typeName; + + ProjectDir = tempDir.CreateSubdirectory(ProjectDirName); + + DocsDir = tempDir.CreateSubdirectory(DocsDirName); + DirectoryInfo docsAssemblyDir = DocsDir.CreateSubdirectory(Namespace); + + string testDataPath = Path.Combine(TestDataRootDirPath, testDataDir); + + string docsOriginalFilePath = Path.Combine(testDataPath, "DocsOriginal.xml"); + string csOriginalFilePath = Path.Combine(testDataPath, "SourceOriginal.cs"); + string csExpectedFilePath = Path.Combine(testDataPath, "SourceExpected.cs"); + string csprojOriginalFilePath = Path.Combine(testDataPath, "Project.csproj"); + + Assert.True(File.Exists(docsOriginalFilePath)); + Assert.True(File.Exists(csOriginalFilePath)); + Assert.True(File.Exists(csExpectedFilePath)); + Assert.True(File.Exists(csprojOriginalFilePath)); + + OriginalFilePath = Path.Combine(docsAssemblyDir.FullName, $"{Type}.xml"); + ActualFilePath = Path.Combine(ProjectDir.FullName, $"{Type}.cs"); + ExpectedFilePath = Path.Combine(tempDir.FullPath, "SourceExpected.cs"); + ProjectFilePath = Path.Combine(ProjectDir.FullName, ProjectFileName); + + File.Copy(docsOriginalFilePath, OriginalFilePath); + File.Copy(csOriginalFilePath, ActualFilePath); + File.Copy(csExpectedFilePath, ExpectedFilePath); + File.Copy(csprojOriginalFilePath, ProjectFilePath); + + Assert.True(File.Exists(OriginalFilePath)); + Assert.True(File.Exists(ActualFilePath)); + Assert.True(File.Exists(ExpectedFilePath)); + Assert.True(File.Exists(ProjectFilePath)); + } + } +} diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs new file mode 100644 index 0000000..c48c75b --- /dev/null +++ b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs @@ -0,0 +1,68 @@ +using System.IO; +using Xunit; +using Microsoft.Build.Locator; + +namespace Libraries.Tests +{ + public class PortToTripleSlashTests + { + [Fact] + public void Port_Basic() + { + PortToTripleSlash("Basic"); + } + + private void PortToTripleSlash( + string testDataDir, + bool save = true, + string assemblyName = TestData.TestAssembly, + string namespaceName = null, // Most namespaces have the same assembly name + string typeName = TestData.TestType) + { + using TestDirectory tempDir = new TestDirectory(); + + PortToTripleSlashTestData testData = new PortToTripleSlashTestData( + tempDir, + testDataDir, + assemblyName: assemblyName, + namespaceName: namespaceName, + typeName: typeName); + + Configuration c = new() + { + Direction = Configuration.PortingDirection.ToTripleSlash, + CsProj = new FileInfo(testData.ProjectFilePath), + Save = save + }; + + c.IncludedAssemblies.Add(assemblyName); + + if (!string.IsNullOrEmpty(namespaceName)) + { + c.IncludedNamespaces.Add(namespaceName); + } + + c.DirsDocsXml.Add(testData.DocsDir); + + var porter = new ToTripleSlashPorter(c); + porter.Start(); + + Verify(testData); + } + + private void Verify(PortToTripleSlashTestData testData) + { + string[] expectedLines = File.ReadAllLines(testData.ExpectedFilePath); + string[] actualLines = File.ReadAllLines(testData.ActualFilePath); + + for (int i = 0; i < expectedLines.Length; i++) + { + string expectedLine = expectedLines[i]; + string actualLine = actualLines[i]; + Assert.Equal(expectedLine, actualLine); + } + + Assert.Equal(expectedLines.Length, actualLines.Length); + } + } +} diff --git a/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml b/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml new file mode 100644 index 0000000..a01fc01 --- /dev/null +++ b/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml @@ -0,0 +1,151 @@ + + + + MyAssembly + 4.0.0.0 + + + System.Object + + + + This is MyClass summary. + + + + + + + + Property + + + MyAssembly + 4.0.0.0 + + + System.Int32 + + + This is the MyProperty summary. + This is the MyProperty value. + + + + + + + + Field + + MyAssembly + 4.0.0.0 + + + System.Int32 + + 1 + + This is the MyField summary. + + + + + + + + Method + + + MyAssembly + 4.0.0.0 + + + System.Int32 + + + + This is the MyIntMethod param1 summary. + This is MyIntMethod summary. + This is MyIntMethod return value. It mentions the . + + . + + ]]> + + This is the ArgumentNullException thrown by MyIntMethod. It mentions the . + This is the IndexOutOfRangeException thrown by MyIntMethod. + + + + + Method + + + MyAssembly + 4.0.0.0 + + + System.Void + + + + This is MyVoidMethod summary. + + This is MyVoidMethod return value. It mentions the . + + + + . + + ]]> + + + + This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . + + This is the IndexOutOfRangeException thrown by MyVoidMethod. + + + + \ No newline at end of file diff --git a/Tests/PortToTripleSlash/TestData/Basic/Project.csproj b/Tests/PortToTripleSlash/TestData/Basic/Project.csproj new file mode 100644 index 0000000..4d7f14e --- /dev/null +++ b/Tests/PortToTripleSlash/TestData/Basic/Project.csproj @@ -0,0 +1,9 @@ + + + + Library + This is MyNamespace description. + net5.0 + + + diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs new file mode 100644 index 0000000..917d4e8 --- /dev/null +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -0,0 +1,87 @@ +using System; + +namespace MyNamespace +{ + public class MyClass + { + /// This is MyClass summary. + public MyClass() + { + } + + internal MyClass(int myProperty) + { + _myProperty = myProperty; + } + + private int _myProperty; + + /// This is the MyProperty summary. + /// This is the MyProperty value. + /// + public int MyProperty + { + get { return _myProperty; } + set { _myProperty = value; } + } + + /// This is the MyField summary. + /// + public int MyField = 1; + + /// This is MyIntMethod summary. + /// This is MyIntMethod return value. It mentions the . + /// . + /// + /// ]]> + /// This is the ArgumentNullException thrown by MyIntMethod. It mentions the . + /// This is the IndexOutOfRangeException thrown by MyIntMethod. + public int MyIntMethod(int param1) + { + return MyField + param1; + } + + /// This is MyVoidMethod summary. + /// This is MyVoidMethod return value. It mentions the . + /// . + /// + /// ]]> + /// /// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . + /// This is the IndexOutOfRangeException thrown by MyVoidMethod. + public void MyVoidMethod() + { + } + } +} diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs new file mode 100644 index 0000000..9aeec80 --- /dev/null +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs @@ -0,0 +1,34 @@ +using System; + +namespace MyNamespace +{ + public class MyClass + { + public MyClass() + { + } + + internal MyClass(int myProperty) + { + _myProperty = myProperty; + } + + private int _myProperty; + public int MyProperty + { + get { return _myProperty; } + set { _myProperty = value; } + } + + public int MyField = 1; + + public int MyIntMethod(int param1) + { + return MyField + param1; + } + + public void MyVoidMethod() + { + } + } +} diff --git a/Tests/TestData.cs b/Tests/TestData.cs new file mode 100644 index 0000000..ac45c7d --- /dev/null +++ b/Tests/TestData.cs @@ -0,0 +1,22 @@ +using System.IO; + +namespace Libraries.Tests +{ + internal class TestData + { + public const string TestAssembly = "MyAssembly"; + public const string TestNamespace = "MyNamespace"; + public const string TestType = "MyType"; + + protected const string DocsDirName = "Docs"; + + protected string Assembly { get; set; } + protected string Namespace { get; set; } + protected string Type { get; set; } + + internal DirectoryInfo DocsDir { get; set; } + internal string OriginalFilePath { get; set; } + internal string ExpectedFilePath { get; set; } + internal string ActualFilePath { get; set; } + } +} diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 5b5a794..b388247 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -2,10 +2,16 @@ net5.0 - + Microsoft + carlossanlop false + + + + + @@ -24,4 +30,10 @@ + + + + + + From f1faa1ad8ff425fc89e76a10acddcde822f3b04c Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 6 Jan 2021 17:53:07 -0800 Subject: [PATCH 15/65] Ensure exceptions are thrown in unit tests when registering MSBuild. Add the Nuget.Frameworks package to the Tests project so that the underlying csproj consumes it without failure. --- Libraries/ToTripleSlashPorter.cs | 19 ++++++++++--------- .../PortToTripleSlashTests.cs | 1 - Tests/Tests.csproj | 1 + 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index 1b7e176..67887fa 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -42,7 +42,7 @@ public void Start() DocsComments.CollectFiles(); if (!DocsComments.Types.Any()) { - Log.ErrorAndExit("No Docs Type APIs found."); + throw new Exception("No Docs Type APIs found."); } Log.Info("Porting from Docs to triple slash..."); @@ -54,8 +54,7 @@ public void Start() } catch (ReflectionTypeLoadException) { - Log.ErrorAndExit("The MSBuild directory was not found in PATH. Use '-MSBuild ' to specify it."); - return; + throw new Exception("The MSBuild directory was not found in PATH. Use '-MSBuild ' to specify it."); } BinaryLogger? binLogger = null; @@ -72,8 +71,7 @@ public void Start() Project? project = workspace.OpenProjectAsync(Config.CsProj!.FullName, msbuildLogger: binLogger).Result; if (project == null) { - Log.ErrorAndExit("Could not find a project."); - return; + throw new Exception("Could not find a project."); } Compilation? compilation = project.GetCompilationAsync().Result; @@ -85,11 +83,14 @@ public void Start() ImmutableList diagnostics = workspace.Diagnostics; if (diagnostics.Any()) { + string allMsgs = Environment.NewLine; foreach (var diagnostic in diagnostics) { - Log.Error($"{diagnostic.Kind} - {diagnostic.Message}"); + string msg = $"{diagnostic.Kind} - {diagnostic.Message}"; + Log.Error(msg); + allMsgs += msg + Environment.NewLine; } - Log.ErrorAndExit("Exiting due to diagnostic errors found."); + throw new Exception("Exiting due to diagnostic errors found: " + allMsgs); } PortCommentsForProject(compilation!); @@ -179,6 +180,7 @@ private static void Register(string searchPath) } }; } + private static Assembly? TryResolveAssemblyFromPaths(AssemblyLoadContext context, AssemblyName assemblyName, string searchPath, Dictionary? knownAssemblyPaths = null) { foreach (var cultureSubfolder in string.IsNullOrEmpty(assemblyName.CultureName) @@ -192,8 +194,7 @@ private static void Register(string searchPath) { foreach (var extension in new[] { "ni.dll", "ni.exe", "dll", "exe" }) { - var candidatePath = Path.Combine( - searchPath, cultureSubfolder, $"{assemblyName.Name}.{extension}"); + var candidatePath = Path.Combine(searchPath, cultureSubfolder, $"{assemblyName.Name}.{extension}"); var isAssemblyLoaded = knownAssemblyPaths?.ContainsKey(candidatePath) == true; if (isAssemblyLoaded || !File.Exists(candidatePath)) diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs index c48c75b..4b5c00e 100644 --- a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs +++ b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs @@ -1,6 +1,5 @@ using System.IO; using Xunit; -using Microsoft.Build.Locator; namespace Libraries.Tests { diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index b388247..ee0ca9b 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -18,6 +18,7 @@ + From 8d39db3a1572025af955d50695c4a3b5602ae2a7 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 6 Jan 2021 18:53:06 -0800 Subject: [PATCH 16/65] Remove log message that exits, instead throw on error. --- Libraries/Configuration.cs | 46 ++++++++++++++++---------------- Libraries/Log.cs | 14 +++++----- Libraries/ToDocsPorter.cs | 4 +-- Libraries/ToTripleSlashPorter.cs | 2 +- 4 files changed, 32 insertions(+), 34 deletions(-) diff --git a/Libraries/Configuration.cs b/Libraries/Configuration.cs index 1af406e..e097e6e 100644 --- a/Libraries/Configuration.cs +++ b/Libraries/Configuration.cs @@ -100,7 +100,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) if (args == null || args.Length == 0) { - Log.ErrorPrintHelpAndExit("No arguments passed to the executable."); + Log.PrintHelpAndError("No arguments passed to the executable."); } Configuration config = new Configuration(); @@ -120,18 +120,18 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) { if (string.IsNullOrWhiteSpace(arg)) { - Log.ErrorAndExit("You must specify a *.csproj path."); + Log.Error("You must specify a *.csproj path."); } else if (!File.Exists(arg)) { - Log.ErrorAndExit($"The *.csproj file does not exist: {arg}"); + Log.Error($"The *.csproj file does not exist: {arg}"); } else { string ext = Path.GetExtension(arg).ToUpperInvariant(); if (ext != ".CSPROJ") { - Log.ErrorAndExit($"The file does not have a *.csproj extension: {arg}"); + Log.Error($"The file does not have a *.csproj extension: {arg}"); } } config.CsProj = new FileInfo(arg); @@ -157,7 +157,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) config.Direction = PortingDirection.ToTripleSlash; break; default: - Log.ErrorAndExit($"Unrecognized direction value: {arg}"); + Log.Error($"Unrecognized direction value: {arg}"); break; } mode = Mode.Initial; @@ -174,7 +174,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) DirectoryInfo dirInfo = new DirectoryInfo(dirPath); if (!dirInfo.Exists) { - Log.ErrorAndExit($"This Docs xml directory does not exist: {dirPath}"); + Log.Error($"This Docs xml directory does not exist: {dirPath}"); } config.DirsDocsXml.Add(dirInfo); @@ -189,11 +189,11 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) { if (!int.TryParse(arg, out int value)) { - Log.ErrorAndExit($"Invalid int value for 'Exception collision threshold' argument: {arg}"); + Log.Error($"Invalid int value for 'Exception collision threshold' argument: {arg}"); } else if (value < 1 || value > 100) { - Log.ErrorAndExit($"Value needs to be between 0 and 100: {value}"); + Log.Error($"Value needs to be between 0 and 100: {value}"); } config.ExceptionCollisionThreshold = value; @@ -218,7 +218,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) } else { - Log.ErrorPrintHelpAndExit("You must specify at least one assembly."); + Log.PrintHelpAndError("You must specify at least one assembly."); } mode = Mode.Initial; @@ -240,7 +240,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) } else { - Log.ErrorPrintHelpAndExit("You must specify at least one namespace."); + Log.PrintHelpAndError("You must specify at least one namespace."); } mode = Mode.Initial; @@ -262,7 +262,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) } else { - Log.ErrorPrintHelpAndExit("You must specify at least one type name."); + Log.PrintHelpAndError("You must specify at least one type name."); } mode = Mode.Initial; @@ -284,7 +284,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) } else { - Log.ErrorPrintHelpAndExit("You must specify at least one assembly."); + Log.PrintHelpAndError("You must specify at least one assembly."); } mode = Mode.Initial; @@ -306,7 +306,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) } else { - Log.ErrorPrintHelpAndExit("You must specify at least one namespace."); + Log.PrintHelpAndError("You must specify at least one namespace."); } mode = Mode.Initial; @@ -328,7 +328,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) } else { - Log.ErrorPrintHelpAndExit("You must specify at least one type name."); + Log.PrintHelpAndError("You must specify at least one type name."); } mode = Mode.Initial; @@ -462,7 +462,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) break; default: - Log.ErrorPrintHelpAndExit($"Unrecognized argument: {arg}"); + Log.PrintHelpAndError($"Unrecognized argument: {arg}"); break; } break; @@ -478,7 +478,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) DirectoryInfo dirInfo = new DirectoryInfo(dirPath); if (!dirInfo.Exists) { - Log.ErrorAndExit($"This IntelliSense directory does not exist: {dirPath}"); + Log.Error($"This IntelliSense directory does not exist: {dirPath}"); } config.DirsIntelliSense.Add(dirInfo); @@ -603,7 +603,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) default: { - Log.ErrorPrintHelpAndExit("Unexpected mode."); + Log.PrintHelpAndError("Unexpected mode."); break; } } @@ -611,19 +611,19 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) if (mode != Mode.Initial) { - Log.ErrorPrintHelpAndExit("You missed an argument value."); + Log.PrintHelpAndError("You missed an argument value."); } if (config.DirsDocsXml == null) { - Log.ErrorPrintHelpAndExit($"You must specify a path to the dotnet-api-docs xml folder using '-{nameof(Mode.Docs)}'."); + Log.PrintHelpAndError($"You must specify a path to the dotnet-api-docs xml folder using '-{nameof(Mode.Docs)}'."); } if (config.Direction == PortingDirection.ToDocs) { if (config.DirsIntelliSense.Count == 0) { - Log.ErrorPrintHelpAndExit($"You must specify at least one IntelliSense & DLL folder using '-{nameof(Mode.IntelliSense)}'."); + Log.PrintHelpAndError($"You must specify at least one IntelliSense & DLL folder using '-{nameof(Mode.IntelliSense)}'."); } } @@ -631,13 +631,13 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) { if (config.CsProj == null) { - Log.ErrorPrintHelpAndExit($"You must specify a *.csproj file using '-{nameof(Mode.CsProj)}'."); + Log.PrintHelpAndError($"You must specify a *.csproj file using '-{nameof(Mode.CsProj)}'."); } } if (config.IncludedAssemblies.Count == 0) { - Log.ErrorPrintHelpAndExit($"You must specify at least one assembly with {nameof(IncludedAssemblies)}."); + Log.PrintHelpAndError($"You must specify at least one assembly with {nameof(IncludedAssemblies)}."); } return config; @@ -648,7 +648,7 @@ private static bool ParseOrExit(string arg, string paramFriendlyName) { if (!bool.TryParse(arg, out bool value)) { - Log.ErrorAndExit($"Invalid boolean value for '{paramFriendlyName}' argument: {arg}"); + Log.Error($"Invalid boolean value for '{paramFriendlyName}' argument: {arg}"); } Log.Cyan($"{paramFriendlyName}:"); diff --git a/Libraries/Log.cs b/Libraries/Log.cs index 5866fe6..822e7cf 100644 --- a/Libraries/Log.cs +++ b/Libraries/Log.cs @@ -102,6 +102,11 @@ public static void Error(string format, params object[]? args) public static void Error(bool endline, string format, params object[]? args) { Print(endline, ConsoleColor.Red, format, args); + + if (args == null) + throw new Exception(format); + else + throw new Exception(string.Format(format, args)); } public static void Cyan(string format) @@ -158,17 +163,10 @@ public static void Line() public delegate void PrintHelpFunction(); - public static void ErrorAndExit(string format, params object[]? args) - { - Error(format, args); - Environment.Exit(0); - } - - public static void ErrorPrintHelpAndExit(string format, params object[]? args) + public static void PrintHelpAndError(string format, params object[]? args) { PrintHelp(); Error(format, args); - Environment.Exit(0); } public static void PrintHelp() diff --git a/Libraries/ToDocsPorter.cs b/Libraries/ToDocsPorter.cs index 38b2d10..a3f87d1 100644 --- a/Libraries/ToDocsPorter.cs +++ b/Libraries/ToDocsPorter.cs @@ -40,13 +40,13 @@ public void Start() if (!IntelliSenseXmlComments.Members.Any()) { - Log.ErrorAndExit("No IntelliSense xml comments found."); + Log.Error("No IntelliSense xml comments found."); } DocsComments.CollectFiles(); if (!DocsComments.Types.Any()) { - Log.ErrorAndExit("No Docs Type APIs found."); + Log.Error("No Docs Type APIs found."); } PortMissingComments(); diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index 67887fa..81edf02 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -42,7 +42,7 @@ public void Start() DocsComments.CollectFiles(); if (!DocsComments.Types.Any()) { - throw new Exception("No Docs Type APIs found."); + Log.Error("No Docs Type APIs found."); } Log.Info("Porting from Docs to triple slash..."); From a3e6e1fa0f85aa9c364b921c2625a1a61fdf30b4 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 6 Jan 2021 18:53:58 -0800 Subject: [PATCH 17/65] Fix minor errors in test files. --- .../TestData/Basic/DocsOriginal.xml | 55 +++++++++++-------- .../TestData/Basic/SourceExpected.cs | 38 ++++++++----- .../TestData/Basic/SourceOriginal.cs | 7 ++- 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml b/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml index a01fc01..3343dbc 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml +++ b/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml @@ -1,5 +1,5 @@ - - + + MyAssembly 4.0.0.0 @@ -9,22 +9,35 @@ - This is MyClass summary. + This is MyType class summary. + ]]> + + + Constructor + + MyAssembly + 4.0.0.0 + + + + This is the MyType constructor summary. + To be added. + + - + Property @@ -38,7 +51,7 @@ Multiple lines. This is the MyProperty summary. This is the MyProperty value. - - + - + Field MyAssembly @@ -77,7 +90,7 @@ Multiple lines. - + Method @@ -90,8 +103,8 @@ Multiple lines. This is the MyIntMethod param1 summary. - This is MyIntMethod summary. - This is MyIntMethod return value. It mentions the . + This is the MyIntMethod summary. + This is the MyIntMethod return value. It mentions the . . - + Method @@ -122,13 +135,10 @@ Mentions the `param1` and the . - This is MyVoidMethod summary. - - This is MyVoidMethod return value. It mentions the . - + This is the MyVoidMethod summary. + This is the MyVoidMethod return value. It mentions the . - - . - ]]> - + ]]> - - This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . - + This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . This is the IndexOutOfRangeException thrown by MyVoidMethod. diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index 917d4e8..ea118a8 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -2,14 +2,24 @@ namespace MyNamespace { - public class MyClass + /// This is MyType class summary. + /// + public class MyType { - /// This is MyClass summary. - public MyClass() + /// This is the MyType constructor summary. + public MyType() { } - internal MyClass(int myProperty) + internal MyType(int myProperty) { _myProperty = myProperty; } @@ -20,11 +30,11 @@ internal MyClass(int myProperty) /// This is the MyProperty value. /// public int MyProperty @@ -45,8 +55,8 @@ public int MyProperty /// ]]> public int MyField = 1; - /// This is MyIntMethod summary. - /// This is MyIntMethod return value. It mentions the . + /// This is the MyIntMethod summary. + /// This is the MyIntMethod return value. It mentions the . /// /// This is the ArgumentNullException thrown by MyIntMethod. It mentions the . - /// This is the IndexOutOfRangeException thrown by MyIntMethod. + /// This is the IndexOutOfRangeException thrown by MyIntMethod. public int MyIntMethod(int param1) { return MyField + param1; } - /// This is MyVoidMethod summary. - /// This is MyVoidMethod return value. It mentions the . + /// This is the MyVoidMethod summary. + /// This is the MyVoidMethod return value. It mentions the . /// . /// /// ]]> - /// /// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . - /// This is the IndexOutOfRangeException thrown by MyVoidMethod. + /// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . + /// This is the IndexOutOfRangeException thrown by MyVoidMethod. public void MyVoidMethod() { } diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs index 9aeec80..d47c025 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs @@ -2,18 +2,19 @@ namespace MyNamespace { - public class MyClass + public class MyType { - public MyClass() + public MyType() { } - internal MyClass(int myProperty) + internal MyType(int myProperty) { _myProperty = myProperty; } private int _myProperty; + public int MyProperty { get { return _myProperty; } From 3c88fedfb938fc6e67dfb171c89c2a4fada61fb4 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 6 Jan 2021 18:54:16 -0800 Subject: [PATCH 18/65] Fix assembly and namespace naming in test code. --- Tests/PortToTripleSlash/PortToTripleSlashTestData.cs | 7 +------ Tests/PortToTripleSlash/PortToTripleSlashTests.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs b/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs index 4d3c004..42a4835 100644 --- a/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs +++ b/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Xunit; namespace Libraries.Tests @@ -12,9 +9,7 @@ internal class PortToTripleSlashTestData : TestData { private string TestDataRootDirPath => @"../../../PortToTripleSlash/TestData"; private const string ProjectDirName = "Project"; - private const string ProjectFileName = "Project.csproj"; private DirectoryInfo ProjectDir { get; set; } - internal string ProjectFilePath { get; set; } internal PortToTripleSlashTestData( @@ -51,7 +46,7 @@ internal PortToTripleSlashTestData( OriginalFilePath = Path.Combine(docsAssemblyDir.FullName, $"{Type}.xml"); ActualFilePath = Path.Combine(ProjectDir.FullName, $"{Type}.cs"); ExpectedFilePath = Path.Combine(tempDir.FullPath, "SourceExpected.cs"); - ProjectFilePath = Path.Combine(ProjectDir.FullName, ProjectFileName); + ProjectFilePath = Path.Combine(ProjectDir.FullName, $"{Assembly}.csproj"); File.Copy(docsOriginalFilePath, OriginalFilePath); File.Copy(csOriginalFilePath, ActualFilePath); diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs index 4b5c00e..d1a9554 100644 --- a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs +++ b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs @@ -15,7 +15,7 @@ private void PortToTripleSlash( string testDataDir, bool save = true, string assemblyName = TestData.TestAssembly, - string namespaceName = null, // Most namespaces have the same assembly name + string namespaceName = TestData.TestNamespace, string typeName = TestData.TestType) { using TestDirectory tempDir = new TestDirectory(); @@ -58,6 +58,13 @@ private void Verify(PortToTripleSlashTestData testData) { string expectedLine = expectedLines[i]; string actualLine = actualLines[i]; + if (System.Diagnostics.Debugger.IsAttached) + { + if (expectedLine != actualLine) + { + System.Diagnostics.Debugger.Break(); + } + } Assert.Equal(expectedLine, actualLine); } From 97de4f3af8f19460eaf1ab7335787b062ba8b6a9 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 6 Jan 2021 22:56:27 -0800 Subject: [PATCH 19/65] Fine tuning detection of some elements. --- Libraries/Extensions.cs | 9 + .../TripleSlashSyntaxRewriter.cs | 154 +++++++++++------- .../TestData/Basic/DocsOriginal.xml | 38 +++-- .../TestData/Basic/SourceExpected.cs | 61 ++++--- .../TestData/Basic/SourceOriginal.cs | 8 +- 5 files changed, 163 insertions(+), 107 deletions(-) diff --git a/Libraries/Extensions.cs b/Libraries/Extensions.cs index 8f92ab7..397b5e5 100644 --- a/Libraries/Extensions.cs +++ b/Libraries/Extensions.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Generic; +using System.Text.RegularExpressions; namespace Libraries { @@ -37,6 +38,14 @@ public static string RemoveSubstrings(this string oldString, params string[] str // Checks if the passed string is considered "empty" according to the Docs repo rules. public static bool IsDocsEmpty(this string? s) => string.IsNullOrWhiteSpace(s) || s == Configuration.ToBeAdded; + + public static string WithoutPrefixes(this string text) + { + return Regex.Replace( + input: text, + pattern: @"(?.*)(?cref=""[A-Z]\:)(?.*)", + replacement: "${left}cref=\"${right}"); + } } } diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index 8432ee4..f80a0d2 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -25,11 +25,6 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod UseBoilerplate = useBoilerplate; } - /// - /// - /// - /// - /// public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) { SyntaxNode? baseNode = base.VisitClassDeclaration(node); @@ -56,8 +51,29 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod public override SyntaxNode? VisitEventDeclaration(EventDeclarationSyntax node) => VisitMemberDeclaration(node); - public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node) => - VisitMemberDeclaration(node); + public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax 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 summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + + return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks); + } + + return node; + } public override SyntaxNode? VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) { @@ -100,7 +116,7 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); - return GetNodeWithTrivia(node, summary, value, remarks, exceptions, seealsos); + return GetNodeWithTrivia(leadingWhitespace, node, summary, value, remarks, exceptions, seealsos); } public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node) @@ -150,23 +166,35 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList summary = GetSummary(summaryText, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(remarksText, leadingWhitespace); - return GetNodeWithTrivia(node, summary, remarks); + return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks); } - private SyntaxNode GetNodeWithTrivia(SyntaxNode node, params SyntaxTriviaList[] trivias) + private SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxNode node, params SyntaxTriviaList[] trivias) { - SyntaxTriviaList finalTrivia = new(SyntaxFactory.CarriageReturnLineFeed); // Space to separate from previous definition + SyntaxTriviaList finalTrivia = new(); + var leadingTrivia = node.GetLeadingTrivia(); + if (leadingTrivia.Any()) + { + if (leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) + { + // Ensure the endline that separates nodes is respected + finalTrivia = new(SyntaxFactory.ElasticCarriageReturnLineFeed); + } + } + foreach (SyntaxTriviaList t in trivias) { finalTrivia = finalTrivia.AddRange(t); } - finalTrivia = finalTrivia.AddRange(GetLeadingWhitespace(node)); // spaces before type declaration + finalTrivia = finalTrivia.AddRange(leadingWhitespace); return node.WithLeadingTrivia(finalTrivia); } 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; @@ -175,27 +203,14 @@ private SyntaxNode GetNodeWithTrivia(SyntaxNode node, params SyntaxTriviaList[] SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); - - SyntaxTriviaList parameters = new(); - foreach (SyntaxTriviaList parameterTrivia in member.Params.Select( - param => GetParam(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) - { - parameters = parameters.AddRange(parameterTrivia); - } - - SyntaxTriviaList typeParameters = new(); - foreach (SyntaxTriviaList typeParameterTrivia in member.TypeParams.Select( - param => GetTypeParam(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) - { - typeParameters = typeParameters.AddRange(typeParameterTrivia); - } - + SyntaxTriviaList parameters = GetParameters(member, leadingWhitespace); + SyntaxTriviaList typeParameters = GetTypeParameters(member, leadingWhitespace); SyntaxTriviaList returns = GetReturns(UseBoilerplate ? BoilerplateText : member.Returns, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); - return GetNodeWithTrivia(node, summary, parameters, typeParameters, returns, remarks, exceptions, seealsos); + return GetNodeWithTrivia(leadingWhitespace, node, summary, parameters, typeParameters, returns, remarks, exceptions, seealsos); } private SyntaxNode? VisitMemberDeclaration(MemberDeclarationSyntax node) @@ -209,19 +224,9 @@ private SyntaxNode GetNodeWithTrivia(SyntaxNode node, params SyntaxTriviaList[] SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); - SyntaxTriviaList exceptions = new(); - // No need to add exceptions in secondary files - if (!UseBoilerplate && member.Exceptions.Any()) - { - foreach (SyntaxTriviaList exceptionsTrivia in member.Exceptions.Select( - exception => GetException(exception.Cref, exception.Value, leadingWhitespace))) - { - exceptions = exceptions.AddRange(exceptionsTrivia); - } - } - - return GetNodeWithTrivia(node, summary, remarks, exceptions); + return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks, exceptions); } private SyntaxTriviaList GetLeadingWhitespace(SyntaxNode node) => @@ -229,7 +234,7 @@ private SyntaxTriviaList GetLeadingWhitespace(SyntaxNode node) => private SyntaxTriviaList GetSummary(string text, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text); + SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); XmlElementSyntax element = SyntaxFactory.XmlSummaryElement(contents); return GetXmlTrivia(element, leadingWhitespace); } @@ -238,7 +243,7 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp { if (!UseBoilerplate && !text.IsDocsEmpty()) { - string trimmedRemarks = text.RemoveSubstrings("").Trim(); // The SyntaxFactory needs to add this + string trimmedRemarks = text.RemoveSubstrings("").Trim(); // The SyntaxFactory needs to be the one to add these SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true); XmlNodeSyntax xmlRemarksContent = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(xmlRemarksContent); @@ -251,25 +256,47 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp private SyntaxTriviaList GetValue(string text, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text); + SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); XmlElementSyntax element = SyntaxFactory.XmlValueElement(contents); return GetXmlTrivia(element, leadingWhitespace); } private SyntaxTriviaList GetParam(string name, string text, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text); + SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); XmlElementSyntax element = SyntaxFactory.XmlParamElement(name, contents); return GetXmlTrivia(element, leadingWhitespace); } private SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace) { - var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute(name, text)); + var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute("name", name)); SyntaxList contents = GetContentsInRows(text); return GetXmlTrivia("typeparam", attribute, contents, leadingWhitespace); } + private SyntaxTriviaList GetParameters(DocsMember member, SyntaxTriviaList leadingWhitespace) + { + SyntaxTriviaList parameters = new(); + foreach (SyntaxTriviaList parameterTrivia in member.Params.Select( + param => GetParam(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) + { + parameters = parameters.AddRange(parameterTrivia); + } + return parameters; + } + + private SyntaxTriviaList GetTypeParameters(DocsMember member, SyntaxTriviaList leadingWhitespace) + { + SyntaxTriviaList typeParameters = new(); + foreach (SyntaxTriviaList typeParameterTrivia in member.TypeParams.Select( + typeParam => GetTypeParam(typeParam.Name, UseBoilerplate ? BoilerplateText : typeParam.Value, leadingWhitespace))) + { + typeParameters = typeParameters.AddRange(typeParameterTrivia); + } + return typeParameters; + } + private SyntaxTriviaList GetReturns(string text, SyntaxTriviaList leadingWhitespace) { // For when returns is empty because the method returns void @@ -278,11 +305,24 @@ private SyntaxTriviaList GetReturns(string text, SyntaxTriviaList leadingWhitesp return new(); } - SyntaxList contents = GetContentsInRows(text); + SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); XmlElementSyntax element = SyntaxFactory.XmlReturnsElement(contents); return GetXmlTrivia(element, leadingWhitespace); } + private SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace) + { + if (cref.Length > 2 && cref[1] == ':') + { + cref = cref[2..]; + } + + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); + XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text.WithoutPrefixes(), leadingWhitespace, addInitialNewLine: false)); + XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); + return GetXmlTrivia(element, leadingWhitespace); + } + private SyntaxTriviaList GetExceptions(DocsMember member, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList exceptions = new(); @@ -298,14 +338,6 @@ private SyntaxTriviaList GetExceptions(DocsMember member, SyntaxTriviaList leadi return exceptions; } - private SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace) - { - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); - XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text, leadingWhitespace, addInitialNewLine: false)); - XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); - return GetXmlTrivia(element, leadingWhitespace); - } - private SyntaxTriviaList GetSeeAlsos(DocsMember member, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList seealsos = new(); @@ -323,6 +355,11 @@ private SyntaxTriviaList GetSeeAlsos(DocsMember member, SyntaxTriviaList leading private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace) { + if (cref.Length > 2 && cref[1] == ':') + { + cref = cref[2..]; + } + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); XmlEmptyElementSyntax element = SyntaxFactory.XmlSeeAlsoElement(crefSyntax); return GetXmlTrivia(element, leadingWhitespace); @@ -358,13 +395,7 @@ private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhi if (splittedLines.Length > 1) { tokens.Add(newLineAndWhitespace); - - if (lineNumber < splittedLines.Length) - { - // New line characters between sentences need to have their own separate line - // but need to avoid adding a final single separate line - tokens.Add(newLineAndWhitespace); - } + tokens.Add(newLineAndWhitespace); } lineNumber++; @@ -426,6 +457,7 @@ private bool TryGetMember(SyntaxNode node, [NotNullWhen(returnValue: true)] out member = DocsComments.Members.FirstOrDefault(m => m.DocId == docId); } } + return member != null; } diff --git a/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml b/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml index 3343dbc..b0e4026 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml +++ b/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml @@ -2,14 +2,9 @@ MyAssembly - 4.0.0.0 - - System.Object - - - This is MyType class summary. + This is the MyType class summary. - + Constructor MyAssembly - 4.0.0.0 - This is the MyType constructor summary. To be added. - + Property MyAssembly - 4.0.0.0 System.Int32 @@ -68,7 +60,6 @@ Multiple lines. Field MyAssembly - 4.0.0.0 System.Int32 @@ -90,19 +81,18 @@ Multiple lines. - + Method MyAssembly - 4.0.0.0 System.Int32 - This is the MyIntMethod param1 summary. + This is the MyIntMethod param2 summary. This is the MyIntMethod summary. This is the MyIntMethod return value. It mentions the . @@ -133,10 +123,8 @@ Mentions the `param1` and the . System.Void - This is the MyVoidMethod summary. - This is the MyVoidMethod return value. It mentions the . . This is the IndexOutOfRangeException thrown by MyVoidMethod. + + + Method + + + MyAssembly + + + System.Void + + + This is the MyTypeParamMethod typeparam T. + This is the MyTypeParamMethod summary. + To be added. + + \ No newline at end of file diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index ea118a8..02fde4d 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -2,15 +2,15 @@ namespace MyNamespace { - /// This is MyType class summary. + /// This is the MyType class summary. /// public class MyType { @@ -29,13 +29,13 @@ internal MyType(int myProperty) /// This is the MyProperty summary. /// This is the MyProperty value. /// public int MyProperty { @@ -45,53 +45,60 @@ public int MyProperty /// This is the MyField summary. /// public int MyField = 1; /// 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 . /// . - /// + /// /// ]]> /// This is the ArgumentNullException thrown by MyIntMethod. It mentions the . /// This is the IndexOutOfRangeException thrown by MyIntMethod. - public int MyIntMethod(int param1) + public int MyIntMethod(int param1, int param2) { - return MyField + param1; + return MyField + param1 + param2; } /// This is the MyVoidMethod summary. - /// This is the MyVoidMethod return value. It mentions the . /// . - /// + /// + /// Mentions the . + /// /// ]]> /// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . /// This is the IndexOutOfRangeException thrown by MyVoidMethod. public void MyVoidMethod() { } + + /// This is the MyTypeParamMethod summary. + /// This is the MyTypeParamMethod typeparam T. + public void MyTypeParamMethod() + { + } } } diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs index d47c025..f5b59f2 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs @@ -23,13 +23,17 @@ public int MyProperty public int MyField = 1; - public int MyIntMethod(int param1) + public int MyIntMethod(int param1, int param2) { - return MyField + param1; + return MyField + param1 + param2; } public void MyVoidMethod() { } + + public void MyTypeParamMethod() + { + } } } From 31ef6ca54850175e70567798c83ad5e4fbb8427b Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 7 Jan 2021 01:08:05 -0800 Subject: [PATCH 20/65] Support altmember, related and seealso. Support event, field and delegate. --- Libraries/Docs/DocsAPI.cs | 52 +++- Libraries/Docs/DocsMember.cs | 20 -- Libraries/Docs/DocsRelated.cs | 39 +++ Libraries/Docs/DocsSeeAlso.cs | 34 --- Libraries/Extensions.cs | 7 +- .../TripleSlashSyntaxRewriter.cs | 254 ++++++++++++------ Tests/PortToDocs/PortToDocsTestData.cs | 19 +- .../PortToTripleSlashTestData.cs | 52 ++-- .../{Project.csproj => MyAssembly.csproj} | 0 .../TestData/Basic/MyDelegate.xml | 22 ++ .../Basic/{DocsOriginal.xml => MyType.xml} | 13 + .../TestData/Basic/SourceExpected.cs | 11 + .../TestData/Basic/SourceOriginal.cs | 4 + Tests/TestData.cs | 17 +- Tests/Tests.csproj | 2 +- 15 files changed, 344 insertions(+), 202 deletions(-) create mode 100644 Libraries/Docs/DocsRelated.cs delete mode 100644 Libraries/Docs/DocsSeeAlso.cs rename Tests/PortToTripleSlash/TestData/Basic/{Project.csproj => MyAssembly.csproj} (100%) create mode 100644 Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml rename Tests/PortToTripleSlash/TestData/Basic/{DocsOriginal.xml => MyType.xml} (92%) diff --git a/Libraries/Docs/DocsAPI.cs b/Libraries/Docs/DocsAPI.cs index 86eead0..64b2a32 100644 --- a/Libraries/Docs/DocsAPI.cs +++ b/Libraries/Docs/DocsAPI.cs @@ -14,7 +14,9 @@ internal abstract class DocsAPI : IDocsAPI private List? _typeParameters; private List? _typeParams; private List? _assemblyInfos; - private List? _seeAlsos; + private List? _seeAlsoCrefs; + private List? _altMemberCrefs; + private List? _relateds; protected readonly XElement XERoot; @@ -122,22 +124,60 @@ public List TypeParams } } - public List SeeAlsos + public List SeeAlsoCrefs { get { - if (_seeAlsos == null) + if (_seeAlsoCrefs == null) { if (Docs != null) { - _seeAlsos = Docs.Elements("seealso").Select(x => new DocsSeeAlso(this, x)).ToList(); + _seeAlsoCrefs = Docs.Elements("seealso").Select(x => XmlHelper.GetAttributeValue(x, "cref")).ToList(); } else { - _seeAlsos = new(); + _seeAlsoCrefs = new(); } } - return _seeAlsos; + return _seeAlsoCrefs; + } + } + + public List AltMembers + { + get + { + if (_altMemberCrefs == null) + { + if (Docs != null) + { + _altMemberCrefs = Docs.Elements("altmember").Select(x => XmlHelper.GetAttributeValue(x, "cref")).ToList(); + } + else + { + _altMemberCrefs = new(); + } + } + return _altMemberCrefs; + } + } + + 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; } } diff --git a/Libraries/Docs/DocsMember.cs b/Libraries/Docs/DocsMember.cs index 1383004..75e5a0a 100644 --- a/Libraries/Docs/DocsMember.cs +++ b/Libraries/Docs/DocsMember.cs @@ -10,7 +10,6 @@ internal class DocsMember : DocsAPI private string? _memberName; private List? _memberSignatures; private string? _docId; - private List? _altMemberCref; private List? _exceptions; public DocsMember(string filePath, DocsType parentType, XElement xeMember) @@ -169,25 +168,6 @@ public string Value } } - public List AltMemberCref - { - get - { - if (_altMemberCref == null) - { - if (Docs != null) - { - _altMemberCref = Docs.Elements("altmember").Select(x => XmlHelper.GetAttributeValue(x, "cref")).ToList(); - } - else - { - _altMemberCref = new List(); - } - } - return _altMemberCref; - } - } - public List Exceptions { get diff --git a/Libraries/Docs/DocsRelated.cs b/Libraries/Docs/DocsRelated.cs new file mode 100644 index 0000000..360b1f6 --- /dev/null +++ b/Libraries/Docs/DocsRelated.cs @@ -0,0 +1,39 @@ +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsRelated + { + private readonly XElement XERelatedArticle; + + public IDocsAPI ParentAPI + { + get; private set; + } + + 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 DocsRelated(IDocsAPI parentAPI, XElement xeRelatedArticle) + { + ParentAPI = parentAPI; + XERelatedArticle = xeRelatedArticle; + } + + public override string ToString() + { + return Value; + } + } +} diff --git a/Libraries/Docs/DocsSeeAlso.cs b/Libraries/Docs/DocsSeeAlso.cs deleted file mode 100644 index ca218b8..0000000 --- a/Libraries/Docs/DocsSeeAlso.cs +++ /dev/null @@ -1,34 +0,0 @@ -#nullable enable -using System.Xml.Linq; - -namespace Libraries.Docs -{ - internal class DocsSeeAlso - { - private readonly XElement XESeeAlso; - - public IDocsAPI ParentAPI - { - get; private set; - } - - public string Cref - { - get - { - return XmlHelper.GetAttributeValue(XESeeAlso, "cref"); - } - } - - public DocsSeeAlso(IDocsAPI parentAPI, XElement xeSeeAlso) - { - ParentAPI = parentAPI; - XESeeAlso = xeSeeAlso; - } - - public override string ToString() - { - return $"{Cref}"; - } - } -} diff --git a/Libraries/Extensions.cs b/Libraries/Extensions.cs index 397b5e5..56c6a7b 100644 --- a/Libraries/Extensions.cs +++ b/Libraries/Extensions.cs @@ -39,8 +39,13 @@ public static string RemoveSubstrings(this string oldString, params string[] str public static bool IsDocsEmpty(this string? s) => string.IsNullOrWhiteSpace(s) || s == Configuration.ToBeAdded; - public static string WithoutPrefixes(this string text) + public static string WithoutPrefix(this string text) { + if (text.Length > 2 && text[1] == ':') + { + return text[2..]; + } + return Regex.Replace( input: text, pattern: @"(?.*)(?cref=""[A-Z]\:)(?.*)", diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index f80a0d2..fe166f8 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -25,6 +25,8 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod UseBoilerplate = useBoilerplate; } + #region Visitor overrides + public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) { SyntaxNode? baseNode = base.VisitClassDeclaration(node); @@ -42,38 +44,31 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod public override SyntaxNode? VisitConstructorDeclaration(ConstructorDeclarationSyntax node) => VisitBaseMethodDeclaration(node); - public override SyntaxNode? VisitEnumDeclaration(EnumDeclarationSyntax node) => - VisitMemberDeclaration(node); - - public override SyntaxNode? VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) => - VisitMemberDeclaration(node); - - public override SyntaxNode? VisitEventDeclaration(EventDeclarationSyntax node) => - VisitMemberDeclaration(node); - - public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node) + public override SyntaxNode? VisitDelegateDeclaration(DelegateDeclarationSyntax node) { - // The comments need to be extracted from the underlying variable declarator inside the declaration - VariableDeclarationSyntax declaration = node.Declaration; + SyntaxNode? baseNode = base.VisitDelegateDeclaration(node); - // Only port docs if there is only one variable in the declaration - if (declaration.Variables.Count == 1) + ISymbol? symbol = Model.GetDeclaredSymbol(node); + if (symbol == null) { - if (!TryGetMember(declaration.Variables.First(), out DocsMember? member)) - { - return node; - } + Log.Warning($"Symbol is null."); + return baseNode; + } - SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); + return VisitType(baseNode, symbol); + } - SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + public override SyntaxNode? VisitEnumDeclaration(EnumDeclarationSyntax node) => + VisitMemberDeclaration(node); - return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks); - } + public override SyntaxNode? VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) => + VisitMemberDeclaration(node); - return node; - } + public override SyntaxNode? VisitEventFieldDeclaration(EventFieldDeclarationSyntax node) => + VisitVariableDeclaration(node); + + public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node) => + VisitVariableDeclaration(node); public override SyntaxNode? VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) { @@ -115,8 +110,10 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); - return GetNodeWithTrivia(leadingWhitespace, node, summary, value, remarks, exceptions, seealsos); + return GetNodeWithTrivia(leadingWhitespace, node, summary, value, remarks, exceptions, seealsos, altmembers, relateds); } public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node) @@ -133,6 +130,8 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod return VisitType(baseNode, symbol); } + #endregion + private SyntaxNode? VisitType(SyntaxNode? node, ISymbol? symbol) { if (node == null || symbol == null) @@ -150,6 +149,14 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod string summaryText = BoilerplateText; string remarksText = string.Empty; + SyntaxTriviaList parameters = new(); + SyntaxTriviaList typeParameters = new(); + SyntaxTriviaList seealsos = new(); + SyntaxTriviaList altmembers = new(); + SyntaxTriviaList relateds = new(); + + SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); + if (!UseBoilerplate) { if (!TryGetType(symbol, out DocsType? type)) @@ -159,36 +166,18 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod summaryText = type.Summary; remarksText = type.Remarks; - } - SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); + parameters = GetParameters(type, leadingWhitespace); + typeParameters = GetTypeParameters(type, leadingWhitespace); + seealsos = GetSeeAlsos(type, leadingWhitespace); + altmembers = GetAltMembers(type, leadingWhitespace); + relateds = GetRelateds(type, leadingWhitespace); + } SyntaxTriviaList summary = GetSummary(summaryText, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(remarksText, leadingWhitespace); - return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks); - } - - private SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxNode node, params SyntaxTriviaList[] trivias) - { - SyntaxTriviaList finalTrivia = new(); - var leadingTrivia = node.GetLeadingTrivia(); - if (leadingTrivia.Any()) - { - if (leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) - { - // Ensure the endline that separates nodes is respected - finalTrivia = new(SyntaxFactory.ElasticCarriageReturnLineFeed); - } - } - - foreach (SyntaxTriviaList t in trivias) - { - finalTrivia = finalTrivia.AddRange(t); - } - finalTrivia = finalTrivia.AddRange(leadingWhitespace); - - return node.WithLeadingTrivia(finalTrivia); + return GetNodeWithTrivia(leadingWhitespace, node, summary, parameters, typeParameters, remarks, seealsos, altmembers, relateds); } private SyntaxNode? VisitBaseMethodDeclaration(BaseMethodDeclarationSyntax node) @@ -209,8 +198,10 @@ private SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxN SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); - return GetNodeWithTrivia(leadingWhitespace, node, summary, parameters, typeParameters, returns, remarks, exceptions, seealsos); + return GetNodeWithTrivia(leadingWhitespace, node, summary, parameters, typeParameters, returns, remarks, exceptions, seealsos, altmembers, relateds); } private SyntaxNode? VisitMemberDeclaration(MemberDeclarationSyntax node) @@ -225,8 +216,60 @@ private SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxN SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); - return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks, exceptions); + return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks, exceptions, 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 summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); + + return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks, seealsos, altmembers, relateds); + } + + return node; + } + + private SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxNode node, params SyntaxTriviaList[] trivias) + { + SyntaxTriviaList finalTrivia = new(); + var leadingTrivia = node.GetLeadingTrivia(); + if (leadingTrivia.Any()) + { + if (leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) + { + // Ensure the endline that separates nodes is respected + finalTrivia = new(SyntaxFactory.ElasticCarriageReturnLineFeed); + } + } + + foreach (SyntaxTriviaList t in trivias) + { + finalTrivia = finalTrivia.AddRange(t); + } + finalTrivia = finalTrivia.AddRange(leadingWhitespace); + + return node.WithLeadingTrivia(finalTrivia); } private SyntaxTriviaList GetLeadingWhitespace(SyntaxNode node) => @@ -234,7 +277,7 @@ private SyntaxTriviaList GetLeadingWhitespace(SyntaxNode node) => private SyntaxTriviaList GetSummary(string text, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); + SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); XmlElementSyntax element = SyntaxFactory.XmlSummaryElement(contents); return GetXmlTrivia(element, leadingWhitespace); } @@ -256,40 +299,40 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp private SyntaxTriviaList GetValue(string text, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); + SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); XmlElementSyntax element = SyntaxFactory.XmlValueElement(contents); return GetXmlTrivia(element, leadingWhitespace); } - private SyntaxTriviaList GetParam(string name, string text, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetParameter(string name, string text, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); + SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); XmlElementSyntax element = SyntaxFactory.XmlParamElement(name, contents); return GetXmlTrivia(element, leadingWhitespace); } - private SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace) - { - var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute("name", name)); - SyntaxList contents = GetContentsInRows(text); - return GetXmlTrivia("typeparam", attribute, contents, leadingWhitespace); - } - - private SyntaxTriviaList GetParameters(DocsMember member, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList parameters = new(); - foreach (SyntaxTriviaList parameterTrivia in member.Params.Select( - param => GetParam(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) + foreach (SyntaxTriviaList parameterTrivia in api.Params.Select( + param => GetParameter(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) { parameters = parameters.AddRange(parameterTrivia); } return parameters; } - private SyntaxTriviaList GetTypeParameters(DocsMember member, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace) + { + var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute("name", name)); + SyntaxList contents = GetContentsInRows(text); + return GetXmlTrivia("typeparam", attribute, contents, leadingWhitespace); + } + + private SyntaxTriviaList GetTypeParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList typeParameters = new(); - foreach (SyntaxTriviaList typeParameterTrivia in member.TypeParams.Select( + foreach (SyntaxTriviaList typeParameterTrivia in api.TypeParams.Select( typeParam => GetTypeParam(typeParam.Name, UseBoilerplate ? BoilerplateText : typeParam.Value, leadingWhitespace))) { typeParameters = typeParameters.AddRange(typeParameterTrivia); @@ -305,20 +348,15 @@ private SyntaxTriviaList GetReturns(string text, SyntaxTriviaList leadingWhitesp return new(); } - SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); + SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); XmlElementSyntax element = SyntaxFactory.XmlReturnsElement(contents); return GetXmlTrivia(element, leadingWhitespace); } private SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace) { - if (cref.Length > 2 && cref[1] == ':') - { - cref = cref[2..]; - } - - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); - XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text.WithoutPrefixes(), leadingWhitespace, addInitialNewLine: false)); + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutPrefix())); + XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text.WithoutPrefix(), leadingWhitespace, addInitialNewLine: false)); XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); return GetXmlTrivia(element, leadingWhitespace); } @@ -338,14 +376,20 @@ private SyntaxTriviaList GetExceptions(DocsMember member, SyntaxTriviaList leadi return exceptions; } - private SyntaxTriviaList GetSeeAlsos(DocsMember member, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace) + { + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutPrefix())); + XmlEmptyElementSyntax element = SyntaxFactory.XmlSeeAlsoElement(crefSyntax); + return GetXmlTrivia(element, leadingWhitespace); + } + + private SyntaxTriviaList GetSeeAlsos(DocsAPI api, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList seealsos = new(); - // No need to add exceptions in secondary files - if (!UseBoilerplate && member.SeeAlsos.Any()) + if (!UseBoilerplate && api.SeeAlsoCrefs.Any()) { - foreach (SyntaxTriviaList seealsoTrivia in member.SeeAlsos.Select( - s => GetSeeAlso(s.Cref, leadingWhitespace))) + foreach (SyntaxTriviaList seealsoTrivia in api.SeeAlsoCrefs.Select( + s => GetSeeAlso(s, leadingWhitespace))) { seealsos = seealsos.AddRange(seealsoTrivia); } @@ -353,16 +397,50 @@ private SyntaxTriviaList GetSeeAlsos(DocsMember member, SyntaxTriviaList leading return seealsos; } - private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetAltMember(string cref, SyntaxTriviaList leadingWhitespace) { - if (cref.Length > 2 && cref[1] == ':') + XmlAttributeSyntax attribute = SyntaxFactory.XmlTextAttribute("cref", cref.WithoutPrefix()); + XmlEmptyElementSyntax emptyElement = SyntaxFactory.XmlEmptyElement(SyntaxFactory.XmlName(SyntaxFactory.Identifier("altmember")), new SyntaxList(attribute)); + return GetXmlTrivia(emptyElement, leadingWhitespace); + } + + private SyntaxTriviaList GetAltMembers(DocsAPI api, SyntaxTriviaList leadingWhitespace) + { + SyntaxTriviaList altMembers = new(); + if (!UseBoilerplate && api.AltMembers.Any()) { - cref = cref[2..]; + foreach (SyntaxTriviaList altMemberTrivia in api.AltMembers.Select( + s => GetAltMember(s, leadingWhitespace))) + { + altMembers = altMembers.AddRange(altMemberTrivia); + } } + return altMembers; + } - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); - XmlEmptyElementSyntax element = SyntaxFactory.XmlSeeAlsoElement(crefSyntax); - return GetXmlTrivia(element, leadingWhitespace); + private SyntaxTriviaList GetRelated(string articleType, string href, string value, SyntaxTriviaList leadingWhitespace) + { + SyntaxList attributes = new(); + + attributes = attributes.Add(SyntaxFactory.XmlTextAttribute("type", articleType)); + attributes = attributes.Add(SyntaxFactory.XmlTextAttribute("href", href)); + + SyntaxList contents = GetContentsInRows(value); + return GetXmlTrivia("related", attributes, contents, leadingWhitespace); + } + + private SyntaxTriviaList GetRelateds(DocsAPI api, SyntaxTriviaList leadingWhitespace) + { + SyntaxTriviaList relateds = new(); + if (!UseBoilerplate && api.Relateds.Any()) + { + foreach (SyntaxTriviaList relatedsTrivia in api.Relateds.Select( + s => GetRelated(s.ArticleType, s.Href, s.Value, leadingWhitespace))) + { + relateds = relateds.AddRange(relatedsTrivia); + } + } + return relateds; } private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace, bool addInitialNewLine) diff --git a/Tests/PortToDocs/PortToDocsTestData.cs b/Tests/PortToDocs/PortToDocsTestData.cs index 5fc16c8..38a6d5d 100644 --- a/Tests/PortToDocs/PortToDocsTestData.cs +++ b/Tests/PortToDocs/PortToDocsTestData.cs @@ -7,12 +7,13 @@ internal class PortToDocsTestData : TestData { private const string TestDataRootDirPath = @"../../../PortToDocs/TestData"; private const string IntellisenseAndDllDirName = "IntelliSenseAndDLL"; - internal DirectoryInfo IntelliSenseAndDLLDir { get; set; } // Docs file with the interface from which the type inherits. internal string InterfaceFilePath { get; set; } + internal string DocsOriginFilePath { get; set; } + internal PortToDocsTestData( TestDirectory tempDir, string testDataDir, @@ -24,15 +25,13 @@ internal PortToDocsTestData( Assert.False(string.IsNullOrWhiteSpace(assemblyName)); Assert.False(string.IsNullOrWhiteSpace(typeName)); - Assembly = assemblyName; - Namespace = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; - Type = typeName; + namespaceName = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; IntelliSenseAndDLLDir = tempDir.CreateSubdirectory(IntellisenseAndDllDirName); - DirectoryInfo tripleSlashAssemblyDir = IntelliSenseAndDLLDir.CreateSubdirectory(Assembly); + DirectoryInfo tripleSlashAssemblyDir = IntelliSenseAndDLLDir.CreateSubdirectory(assemblyName); DocsDir = tempDir.CreateSubdirectory(DocsDirName); - DirectoryInfo docsAssemblyDir = DocsDir.CreateSubdirectory(Namespace); + DirectoryInfo docsAssemblyDir = DocsDir.CreateSubdirectory(namespaceName); string testDataPath = Path.Combine(TestDataRootDirPath, testDataDir); @@ -44,15 +43,15 @@ internal PortToDocsTestData( Assert.True(File.Exists(docsOriginalFilePath)); Assert.True(File.Exists(docsExpectedFilePath)); - OriginalFilePath = Path.Combine(tripleSlashAssemblyDir.FullName, $"{Type}.xml"); - ActualFilePath = Path.Combine(docsAssemblyDir.FullName, $"{Type}.xml"); + DocsOriginFilePath = Path.Combine(tripleSlashAssemblyDir.FullName, $"{typeName}.xml"); + ActualFilePath = Path.Combine(docsAssemblyDir.FullName, $"{typeName}.xml"); ExpectedFilePath = Path.Combine(tempDir.FullPath, "DocsExpected.xml"); - File.Copy(tripleSlashOriginalFilePath, OriginalFilePath); + File.Copy(tripleSlashOriginalFilePath, DocsOriginFilePath); File.Copy(docsOriginalFilePath, ActualFilePath); File.Copy(docsExpectedFilePath, ExpectedFilePath); - Assert.True(File.Exists(OriginalFilePath)); + Assert.True(File.Exists(DocsOriginFilePath)); Assert.True(File.Exists(ActualFilePath)); Assert.True(File.Exists(ExpectedFilePath)); diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs b/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs index 42a4835..9357b2b 100644 --- a/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs +++ b/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; +using System.IO; using Xunit; namespace Libraries.Tests @@ -22,41 +20,33 @@ internal PortToTripleSlashTestData( Assert.False(string.IsNullOrWhiteSpace(assemblyName)); Assert.False(string.IsNullOrWhiteSpace(typeName)); - Assembly = assemblyName; - Namespace = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; - Type = typeName; + namespaceName = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; ProjectDir = tempDir.CreateSubdirectory(ProjectDirName); DocsDir = tempDir.CreateSubdirectory(DocsDirName); - DirectoryInfo docsAssemblyDir = DocsDir.CreateSubdirectory(Namespace); + DirectoryInfo docsAssemblyDir = DocsDir.CreateSubdirectory(namespaceName); string testDataPath = Path.Combine(TestDataRootDirPath, testDataDir); - string docsOriginalFilePath = Path.Combine(testDataPath, "DocsOriginal.xml"); - string csOriginalFilePath = Path.Combine(testDataPath, "SourceOriginal.cs"); - string csExpectedFilePath = Path.Combine(testDataPath, "SourceExpected.cs"); - string csprojOriginalFilePath = Path.Combine(testDataPath, "Project.csproj"); - - Assert.True(File.Exists(docsOriginalFilePath)); - Assert.True(File.Exists(csOriginalFilePath)); - Assert.True(File.Exists(csExpectedFilePath)); - Assert.True(File.Exists(csprojOriginalFilePath)); - - OriginalFilePath = Path.Combine(docsAssemblyDir.FullName, $"{Type}.xml"); - ActualFilePath = Path.Combine(ProjectDir.FullName, $"{Type}.cs"); - ExpectedFilePath = Path.Combine(tempDir.FullPath, "SourceExpected.cs"); - ProjectFilePath = Path.Combine(ProjectDir.FullName, $"{Assembly}.csproj"); - - File.Copy(docsOriginalFilePath, OriginalFilePath); - File.Copy(csOriginalFilePath, ActualFilePath); - File.Copy(csExpectedFilePath, ExpectedFilePath); - File.Copy(csprojOriginalFilePath, ProjectFilePath); - - Assert.True(File.Exists(OriginalFilePath)); - Assert.True(File.Exists(ActualFilePath)); - Assert.True(File.Exists(ExpectedFilePath)); - Assert.True(File.Exists(ProjectFilePath)); + foreach (string origin in Directory.EnumerateFiles(testDataPath, "*.xml")) + { + string fileName = Path.GetFileName(origin); + string destination = Path.Combine(docsAssemblyDir.FullName, fileName); + File.Copy(origin, destination); + } + + string originCsOriginal = Path.Combine(testDataPath, $"SourceOriginal.cs"); + ActualFilePath = Path.Combine(ProjectDir.FullName, $"{typeName}.cs"); + File.Copy(originCsOriginal, ActualFilePath); + + string originCsExpected = Path.Combine(testDataPath, $"SourceExpected.cs"); + ExpectedFilePath = Path.Combine(tempDir.FullPath, $"SourceExpected.cs"); + File.Copy(originCsExpected, ExpectedFilePath); + + string originCsproj = Path.Combine(testDataPath, $"{assemblyName}.csproj"); + ProjectFilePath = Path.Combine(ProjectDir.FullName, $"{assemblyName}.csproj"); + File.Copy(originCsproj, ProjectFilePath); } } } diff --git a/Tests/PortToTripleSlash/TestData/Basic/Project.csproj b/Tests/PortToTripleSlash/TestData/Basic/MyAssembly.csproj similarity index 100% rename from Tests/PortToTripleSlash/TestData/Basic/Project.csproj rename to Tests/PortToTripleSlash/TestData/Basic/MyAssembly.csproj diff --git a/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml b/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml new file mode 100644 index 0000000..42af915 --- /dev/null +++ b/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml @@ -0,0 +1,22 @@ + + + + MyAssembly + + + + + + + System.Void + + + This is the sender parameter. + This is the e parameter. + This is the MyDelegate summary. + To be added. + + + The .NET Runtime repo. + + \ No newline at end of file diff --git a/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml similarity index 92% rename from Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml rename to Tests/PortToTripleSlash/TestData/Basic/MyType.xml index b0e4026..c40b945 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml +++ b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml @@ -158,5 +158,18 @@ Mentions the . To be added. + + + Event + + MyAssembly + + + MyNamespace.MyDelegate + + + This is the MyEvent summary. + + \ No newline at end of file diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index 02fde4d..d10f687 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -100,5 +100,16 @@ public void MyVoidMethod() public void MyTypeParamMethod() { } + + /// This is the MyDelegate summary. + /// This is the sender parameter. + /// This is the e parameter. + /// + /// + /// The .NET Runtime repo. + public delegate void MyDelegate(object sender, object e); + + /// This is the MyEvent summary. + public event MyDelegate MyEvent; } } diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs index f5b59f2..2993f5e 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs @@ -35,5 +35,9 @@ public void MyVoidMethod() public void MyTypeParamMethod() { } + + public delegate void MyDelegate(object sender, object e); + + public event MyDelegate MyEvent; } } diff --git a/Tests/TestData.cs b/Tests/TestData.cs index ac45c7d..ca56dd5 100644 --- a/Tests/TestData.cs +++ b/Tests/TestData.cs @@ -4,19 +4,14 @@ namespace Libraries.Tests { internal class TestData { - public const string TestAssembly = "MyAssembly"; - public const string TestNamespace = "MyNamespace"; - public const string TestType = "MyType"; + internal const string TestAssembly = "MyAssembly"; + internal const string TestNamespace = "MyNamespace"; + internal const string TestType = "MyType"; + internal const string DocsDirName = "Docs"; - protected const string DocsDirName = "Docs"; - - protected string Assembly { get; set; } - protected string Namespace { get; set; } - protected string Type { get; set; } - - internal DirectoryInfo DocsDir { get; set; } - internal string OriginalFilePath { get; set; } internal string ExpectedFilePath { get; set; } internal string ActualFilePath { get; set; } + internal DirectoryInfo DocsDir { get; set; } + } } diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index ee0ca9b..fdf6811 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -34,7 +34,7 @@ - + From f5e17133cd32611e61445f4f301d95875a7d588f Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 7 Jan 2021 15:51:39 -0800 Subject: [PATCH 21/65] Fix some more exceptions vs errors. Remove boilerplate message. Avoid adding ## Remarks. Fix bug preventing exception comments from being added. --- Libraries/Configuration.cs | 20 +-- Libraries/Docs/DocsCommentsContainer.cs | 3 +- Libraries/Docs/DocsMember.cs | 3 +- Libraries/Docs/DocsType.cs | 3 +- .../IntelliSenseXmlCommentsContainer.cs | 3 +- Libraries/Log.cs | 20 ++- .../TripleSlashSyntaxRewriter.cs | 142 ++++++++---------- Libraries/ToDocsPorter.cs | 2 +- Libraries/ToTripleSlashPorter.cs | 40 +++-- Libraries/XmlHelper.cs | 27 ++-- .../PortToTripleSlashTests.cs | 4 +- .../TestData/Basic/SourceExpected.cs | 10 -- 12 files changed, 133 insertions(+), 144 deletions(-) diff --git a/Libraries/Configuration.cs b/Libraries/Configuration.cs index e097e6e..9424909 100644 --- a/Libraries/Configuration.cs +++ b/Libraries/Configuration.cs @@ -120,18 +120,18 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) { if (string.IsNullOrWhiteSpace(arg)) { - Log.Error("You must specify a *.csproj path."); + throw new Exception("You must specify a *.csproj path."); } else if (!File.Exists(arg)) { - Log.Error($"The *.csproj file does not exist: {arg}"); + throw new Exception($"The *.csproj file does not exist: {arg}"); } else { string ext = Path.GetExtension(arg).ToUpperInvariant(); if (ext != ".CSPROJ") { - Log.Error($"The file does not have a *.csproj extension: {arg}"); + throw new Exception($"The file does not have a *.csproj extension: {arg}"); } } config.CsProj = new FileInfo(arg); @@ -155,9 +155,11 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) break; case "TOTRIPLESLASH": config.Direction = PortingDirection.ToTripleSlash; + // Must always skip to avoid loading interface docs files to memory + config.SkipInterfaceImplementations = true; break; default: - Log.Error($"Unrecognized direction value: {arg}"); + throw new Exception($"Unrecognized direction value: {arg}"); break; } mode = Mode.Initial; @@ -174,7 +176,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) DirectoryInfo dirInfo = new DirectoryInfo(dirPath); if (!dirInfo.Exists) { - Log.Error($"This Docs xml directory does not exist: {dirPath}"); + throw new Exception($"This Docs xml directory does not exist: {dirPath}"); } config.DirsDocsXml.Add(dirInfo); @@ -189,11 +191,11 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) { if (!int.TryParse(arg, out int value)) { - Log.Error($"Invalid int value for 'Exception collision threshold' argument: {arg}"); + throw new Exception($"Invalid int value for 'Exception collision threshold' argument: {arg}"); } else if (value < 1 || value > 100) { - Log.Error($"Value needs to be between 0 and 100: {value}"); + throw new Exception($"Value needs to be between 0 and 100: {value}"); } config.ExceptionCollisionThreshold = value; @@ -478,7 +480,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) DirectoryInfo dirInfo = new DirectoryInfo(dirPath); if (!dirInfo.Exists) { - Log.Error($"This IntelliSense directory does not exist: {dirPath}"); + throw new Exception($"This IntelliSense directory does not exist: {dirPath}"); } config.DirsIntelliSense.Add(dirInfo); @@ -648,7 +650,7 @@ private static bool ParseOrExit(string arg, string paramFriendlyName) { if (!bool.TryParse(arg, out bool value)) { - Log.Error($"Invalid boolean value for '{paramFriendlyName}' argument: {arg}"); + throw new Exception($"Invalid boolean value for '{paramFriendlyName}' argument: {arg}"); } Log.Cyan($"{paramFriendlyName}:"); diff --git a/Libraries/Docs/DocsCommentsContainer.cs b/Libraries/Docs/DocsCommentsContainer.cs index 0d9fc44..958ee61 100644 --- a/Libraries/Docs/DocsCommentsContainer.cs +++ b/Libraries/Docs/DocsCommentsContainer.cs @@ -175,8 +175,7 @@ private void LoadFile(FileInfo fileInfo) { if (!fileInfo.Exists) { - Log.Error($"Docs xml file does not exist: {fileInfo.FullName}"); - return; + throw new Exception($"Docs xml file does not exist: {fileInfo.FullName}"); } xDoc = XDocument.Load(fileInfo.FullName); diff --git a/Libraries/Docs/DocsMember.cs b/Libraries/Docs/DocsMember.cs index 75e5a0a..4b919a3 100644 --- a/Libraries/Docs/DocsMember.cs +++ b/Libraries/Docs/DocsMember.cs @@ -63,8 +63,7 @@ public override string DocId if (ms == null) { string message = string.Format("Could not find a DocId MemberSignature for '{0}'", MemberName); - Log.Error(message); - throw new MissingMemberException(message); + throw new Exception(message); } _docId = ms.Value; } diff --git a/Libraries/Docs/DocsType.cs b/Libraries/Docs/DocsType.cs index 6731f87..6c89ccf 100644 --- a/Libraries/Docs/DocsType.cs +++ b/Libraries/Docs/DocsType.cs @@ -90,8 +90,7 @@ public override string DocId if (dts == null) { string message = $"DocId TypeSignature not found for FullName"; - Log.Error($"DocId TypeSignature not found for FullName"); - throw new MissingMemberException(message); + throw new Exception($"DocId TypeSignature not found for FullName"); } _docId = dts.Value; } diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs index 9a167ba..7a9ce6b 100644 --- a/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs @@ -1,4 +1,5 @@ #nullable enable +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -86,7 +87,7 @@ private void LoadFile(FileInfo fileInfo, bool printSuccess) { if (!fileInfo.Exists) { - Log.Error($"The IntelliSense xml file does not exist: {fileInfo.FullName}"); + throw new Exception($"The IntelliSense xml file does not exist: {fileInfo.FullName}"); return; } diff --git a/Libraries/Log.cs b/Libraries/Log.cs index 822e7cf..f98cd79 100644 --- a/Libraries/Log.cs +++ b/Libraries/Log.cs @@ -102,11 +102,6 @@ public static void Error(string format, params object[]? args) public static void Error(bool endline, string format, params object[]? args) { Print(endline, ConsoleColor.Red, format, args); - - if (args == null) - throw new Exception(format); - else - throw new Exception(string.Format(format, args)); } public static void Cyan(string format) @@ -139,6 +134,11 @@ public static void Cyan(bool endline, string format, params object[]? args) Print(endline, ConsoleColor.Cyan, format, args); } + public static void Assert(bool condition, string format) + { + Assert(true, condition, format, null); + } + public static void Assert(bool condition, string format, params object[]? args) { Assert(true, condition, format, args); @@ -152,7 +152,8 @@ public static void Assert(bool endline, bool condition, string format, params ob } else { - Error(endline, format, args); + string msg = args != null ? string.Format(format, args) : format; + throw new Exception(msg); } } @@ -167,6 +168,11 @@ public static void PrintHelpAndError(string format, params object[]? args) { PrintHelp(); Error(format, args); + + if (args == null) + throw new Exception(format); + else + throw new Exception(string.Format(format, args)); } public static void PrintHelp() @@ -247,6 +253,8 @@ Determines in which direction the comments should flow. to the specified Docs repo containing ECMA xml files. > ToTripleSlash: Comments are ported from the specified Docs repo containint ECMA xml files, to the triple slash comments on top of each API in the specified source code repo. + Using this option automatically sets `SkipInterfaceImplementations` to `true`, to avoid loading + unnecessary interface docs xml files into memory. Usage example: -Direction ToTripleSlash diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index fe166f8..cce596a 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -12,17 +12,13 @@ namespace Libraries.RoslynTripleSlash { internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter { - private const string BoilerplateText = "Comments located in main file."; - private DocsCommentsContainer DocsComments { get; } private SemanticModel Model { get; } - private bool UseBoilerplate { get; } - public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticModel model, Location location, SyntaxTree tree, bool useBoilerplate) : base(visitIntoStructuredTrivia: true) + public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticModel model, Location location, SyntaxTree tree) : base(visitIntoStructuredTrivia: true) { DocsComments = docsComments; Model = model; - UseBoilerplate = useBoilerplate; } #region Visitor overrides @@ -94,24 +90,15 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod return node; } - string summaryText = BoilerplateText; - string valueText = BoilerplateText; - - if (!UseBoilerplate) - { - summaryText = member.Summary; - valueText = member.Value; - } - SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(summaryText, leadingWhitespace); - SyntaxTriviaList value = GetValue(valueText, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); + SyntaxTriviaList value = GetValue(member.Value, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); - SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); - SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); - SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); - SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); + SyntaxTriviaList exceptions = GetExceptions(member.Exceptions, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace); return GetNodeWithTrivia(leadingWhitespace, node, summary, value, remarks, exceptions, seealsos, altmembers, relateds); } @@ -146,36 +133,22 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod return node; } - string summaryText = BoilerplateText; - string remarksText = string.Empty; - - SyntaxTriviaList parameters = new(); - SyntaxTriviaList typeParameters = new(); - SyntaxTriviaList seealsos = new(); - SyntaxTriviaList altmembers = new(); - SyntaxTriviaList relateds = new(); - SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - if (!UseBoilerplate) + if (!TryGetType(symbol, out DocsType? type)) { - if (!TryGetType(symbol, out DocsType? type)) - { - return node; - } - - summaryText = type.Summary; - remarksText = type.Remarks; - - parameters = GetParameters(type, leadingWhitespace); - typeParameters = GetTypeParameters(type, leadingWhitespace); - seealsos = GetSeeAlsos(type, leadingWhitespace); - altmembers = GetAltMembers(type, leadingWhitespace); - relateds = GetRelateds(type, leadingWhitespace); + return node; } - SyntaxTriviaList summary = GetSummary(summaryText, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(remarksText, leadingWhitespace); + + SyntaxTriviaList summary = GetSummary(type.Summary, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(type.Remarks, leadingWhitespace); + SyntaxTriviaList parameters = GetParameters(type, leadingWhitespace); + SyntaxTriviaList typeParameters = GetTypeParameters(type, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(type.SeeAlsoCrefs, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(type.AltMembers, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(type.Relateds, leadingWhitespace); + return GetNodeWithTrivia(leadingWhitespace, node, summary, parameters, typeParameters, remarks, seealsos, altmembers, relateds); } @@ -191,15 +164,15 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); SyntaxTriviaList parameters = GetParameters(member, leadingWhitespace); SyntaxTriviaList typeParameters = GetTypeParameters(member, leadingWhitespace); - SyntaxTriviaList returns = GetReturns(UseBoilerplate ? BoilerplateText : member.Returns, leadingWhitespace); + SyntaxTriviaList returns = GetReturns(member.Returns, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); - SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); - SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); - SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); - SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); + SyntaxTriviaList exceptions = GetExceptions(member.Exceptions, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace); return GetNodeWithTrivia(leadingWhitespace, node, summary, parameters, typeParameters, returns, remarks, exceptions, seealsos, altmembers, relateds); } @@ -213,12 +186,12 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); - SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); - SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); - SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); - SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); + SyntaxTriviaList exceptions = GetExceptions(member.Exceptions, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace); return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks, exceptions, seealsos, altmembers, relateds); } @@ -238,11 +211,11 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); - SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); - SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); - SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace); return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks, seealsos, altmembers, relateds); } @@ -284,10 +257,10 @@ private SyntaxTriviaList GetSummary(string text, SyntaxTriviaList leadingWhitesp private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitespace) { - if (!UseBoilerplate && !text.IsDocsEmpty()) + if (!text.IsDocsEmpty()) { string trimmedRemarks = text.RemoveSubstrings("").Trim(); // The SyntaxFactory needs to be the one to add these - SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true); + SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true, removeRemarksHeader: true); XmlNodeSyntax xmlRemarksContent = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(xmlRemarksContent); @@ -315,7 +288,7 @@ private SyntaxTriviaList GetParameters(DocsAPI api, SyntaxTriviaList leadingWhit { SyntaxTriviaList parameters = new(); foreach (SyntaxTriviaList parameterTrivia in api.Params.Select( - param => GetParameter(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) + param => GetParameter(param.Name, param.Value, leadingWhitespace))) { parameters = parameters.AddRange(parameterTrivia); } @@ -333,7 +306,7 @@ private SyntaxTriviaList GetTypeParameters(DocsAPI api, SyntaxTriviaList leading { SyntaxTriviaList typeParameters = new(); foreach (SyntaxTriviaList typeParameterTrivia in api.TypeParams.Select( - typeParam => GetTypeParam(typeParam.Name, UseBoilerplate ? BoilerplateText : typeParam.Value, leadingWhitespace))) + typeParam => GetTypeParam(typeParam.Name, typeParam.Value, leadingWhitespace))) { typeParameters = typeParameters.AddRange(typeParameterTrivia); } @@ -361,13 +334,13 @@ private SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList return GetXmlTrivia(element, leadingWhitespace); } - private SyntaxTriviaList GetExceptions(DocsMember member, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetExceptions(List docsExceptions, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList exceptions = new(); // No need to add exceptions in secondary files - if (!UseBoilerplate && member.Exceptions.Any()) + if (docsExceptions.Any()) { - foreach (SyntaxTriviaList exceptionsTrivia in member.Exceptions.Select( + foreach (SyntaxTriviaList exceptionsTrivia in docsExceptions.Select( exception => GetException(exception.Cref, exception.Value, leadingWhitespace))) { exceptions = exceptions.AddRange(exceptionsTrivia); @@ -383,12 +356,12 @@ private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitesp return GetXmlTrivia(element, leadingWhitespace); } - private SyntaxTriviaList GetSeeAlsos(DocsAPI api, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetSeeAlsos(List docsSeeAlsoCrefs, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList seealsos = new(); - if (!UseBoilerplate && api.SeeAlsoCrefs.Any()) + if (docsSeeAlsoCrefs.Any()) { - foreach (SyntaxTriviaList seealsoTrivia in api.SeeAlsoCrefs.Select( + foreach (SyntaxTriviaList seealsoTrivia in docsSeeAlsoCrefs.Select( s => GetSeeAlso(s, leadingWhitespace))) { seealsos = seealsos.AddRange(seealsoTrivia); @@ -404,12 +377,12 @@ private SyntaxTriviaList GetAltMember(string cref, SyntaxTriviaList leadingWhite return GetXmlTrivia(emptyElement, leadingWhitespace); } - private SyntaxTriviaList GetAltMembers(DocsAPI api, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetAltMembers(List docsAltMembers, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList altMembers = new(); - if (!UseBoilerplate && api.AltMembers.Any()) + if (docsAltMembers.Any()) { - foreach (SyntaxTriviaList altMemberTrivia in api.AltMembers.Select( + foreach (SyntaxTriviaList altMemberTrivia in docsAltMembers.Select( s => GetAltMember(s, leadingWhitespace))) { altMembers = altMembers.AddRange(altMemberTrivia); @@ -429,12 +402,12 @@ private SyntaxTriviaList GetRelated(string articleType, string href, string valu return GetXmlTrivia("related", attributes, contents, leadingWhitespace); } - private SyntaxTriviaList GetRelateds(DocsAPI api, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetRelateds(List docsRelateds, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList relateds = new(); - if (!UseBoilerplate && api.Relateds.Any()) + if (docsRelateds.Any()) { - foreach (SyntaxTriviaList relatedsTrivia in api.Relateds.Select( + foreach (SyntaxTriviaList relatedsTrivia in docsRelateds.Select( s => GetRelated(s.ArticleType, s.Href, s.Value, leadingWhitespace))) { relateds = relateds.AddRange(relatedsTrivia); @@ -443,7 +416,7 @@ private SyntaxTriviaList GetRelateds(DocsAPI api, SyntaxTriviaList leadingWhites return relateds; } - private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace, bool addInitialNewLine) + private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace, bool addInitialNewLine, bool removeRemarksHeader = false) { string whitespace = leadingWhitespace.ToFullString().Replace(Environment.NewLine, ""); SyntaxToken newLineAndWhitespace = SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace); @@ -463,9 +436,18 @@ private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhi tokens.Add(newLineAndWhitespace); } - int lineNumber = 1; - foreach (string line in splittedLines) + for (int lineNumber = 0; lineNumber < splittedLines.Length; lineNumber++) { + string line = splittedLines[lineNumber]; + + if (removeRemarksHeader && + (line.Contains("## Remarks") || line.Contains("##Remarks"))) + { + // Avoid adding the '## Remarks' header, it's unnecessary + removeRemarksHeader = false; + continue; + } + SyntaxToken token = SyntaxFactory.XmlTextLiteral(leading, line, line, default); tokens.Add(token); @@ -475,8 +457,6 @@ private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhi tokens.Add(newLineAndWhitespace); tokens.Add(newLineAndWhitespace); } - - lineNumber++; } return SyntaxFactory.TokenList(tokens); } diff --git a/Libraries/ToDocsPorter.cs b/Libraries/ToDocsPorter.cs index a3f87d1..67828a3 100644 --- a/Libraries/ToDocsPorter.cs +++ b/Libraries/ToDocsPorter.cs @@ -313,7 +313,7 @@ private void TryPortMissingParamsForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlMe created = TryPromptParam(dParam, tsMemberToPort, out IntelliSenseXmlParam? newTsParam); if (newTsParam == null) { - Log.Error($" There param '{dParam.Name}' was not found in IntelliSense xml for {dApiToUpdate.DocId}"); + Log.Error($" The param '{dParam.Name}' was not found in IntelliSense xml for {dApiToUpdate.DocId}."); } else { diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index 81edf02..0f70755 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -1,17 +1,16 @@ #nullable enable using Libraries.Docs; using Libraries.RoslynTripleSlash; +using Microsoft.Build.Locator; using Microsoft.Build.Logging; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.MSBuild; using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Reflection; -using System.Linq; -using Microsoft.Build.Locator; -using System.Collections.Generic; using System.Runtime.Loader; namespace Libraries @@ -57,6 +56,8 @@ public void Start() throw new Exception("The MSBuild directory was not found in PATH. Use '-MSBuild ' to specify it."); } + CheckDiagnostics(workspace, "MSBuildWorkspace.Create"); + BinaryLogger? binLogger = null; if (Config.BinLogger) { @@ -74,26 +75,45 @@ public void Start() throw new Exception("Could not find a project."); } + CheckDiagnostics(workspace, "workspace.OpenProjectAsync"); + Compilation? compilation = project.GetCompilationAsync().Result; if (compilation == null) { throw new NullReferenceException("The project's compilation was null."); } + CheckDiagnostics(workspace, "project.GetCompilationAsync"); + + PortCommentsForProject(compilation!); + } + + private void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) + { ImmutableList diagnostics = workspace.Diagnostics; if (diagnostics.Any()) { - string allMsgs = Environment.NewLine; + string initialMsg = $"Diagnostic messages found in {stepName}:"; + Log.Error(initialMsg); + + List allMsgs = new() { initialMsg }; + foreach (var diagnostic in diagnostics) { string msg = $"{diagnostic.Kind} - {diagnostic.Message}"; Log.Error(msg); - allMsgs += msg + Environment.NewLine; + + if (!msg.Contains("Warning - Found project reference without a matching metadata reference")) + { + allMsgs.Add(msg); + } } - throw new Exception("Exiting due to diagnostic errors found: " + allMsgs); - } - PortCommentsForProject(compilation!); + if (allMsgs.Count > 1) + { + throw new Exception("Exiting due to diagnostic errors found: " + Environment.NewLine + string.Join(Environment.NewLine, allMsgs)); + } + } } private void PortCommentsForProject(Compilation compilation) @@ -116,7 +136,6 @@ private void PortCommentsForProject(Compilation compilation) private void PortCommentsForType(Compilation compilation, IDocsAPI api, ISymbol symbol) { - bool useBoilerplate = false; foreach (Location location in symbol.Locations) { SyntaxTree? tree = location.SourceTree; @@ -127,7 +146,7 @@ private void PortCommentsForType(Compilation compilation, IDocsAPI api, ISymbol } SemanticModel model = compilation.GetSemanticModel(tree); - var rewriter = new TripleSlashSyntaxRewriter(DocsComments, model, location, tree, useBoilerplate); + var rewriter = new TripleSlashSyntaxRewriter(DocsComments, model, location, tree); SyntaxNode? newRoot = rewriter.Visit(tree.GetRoot()); if (newRoot == null) { @@ -136,7 +155,6 @@ private void PortCommentsForType(Compilation compilation, IDocsAPI api, ISymbol } File.WriteAllText(tree.FilePath, newRoot.ToFullString()); - useBoilerplate = true; } } diff --git a/Libraries/XmlHelper.cs b/Libraries/XmlHelper.cs index cbf6a6c..198e667 100644 --- a/Libraries/XmlHelper.cs +++ b/Libraries/XmlHelper.cs @@ -87,8 +87,7 @@ public static string GetAttributeValue(XElement parent, string name) { if (parent == null) { - Log.Error("A null parent was passed when attempting to get attribute '{0}'", name); - throw new ArgumentNullException(nameof(parent)); + throw new Exception($"A null parent was passed when attempting to get attribute '{name}'"); } else { @@ -129,8 +128,7 @@ public static string GetNodesInPlainText(XElement element) { if (element == null) { - Log.Error("A null element was passed when attempting to retrieve the nodes in plain text."); - throw new ArgumentNullException(nameof(element)); + throw new Exception("A null element was passed when attempting to retrieve the nodes in plain text."); } return string.Join("", element.Nodes()).Trim(); } @@ -139,8 +137,7 @@ public static void SaveFormattedAsMarkdown(XElement element, string newValue, bo { if (element == null) { - Log.Error("A null element was passed when attempting to save formatted as markdown"); - throw new ArgumentNullException(nameof(element)); + 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 @@ -172,14 +169,12 @@ public static void AddChildFormattedAsMarkdown(XElement parent, XElement child, { if (parent == null) { - Log.Error("A null parent was passed when attempting to add child formatted as markdown"); - throw new ArgumentNullException(nameof(parent)); + throw new Exception("A null parent was passed when attempting to add child formatted as markdown."); } if (child == null) { - Log.Error("A null child was passed when attempting to add child formatted as markdown"); - throw new ArgumentNullException(nameof(child)); + throw new Exception("A null child was passed when attempting to add child formatted as markdown."); } SaveFormattedAsMarkdown(child, childValue, isMember); @@ -190,8 +185,7 @@ public static void SaveFormattedAsXml(XElement element, string newValue, bool re { if (element == null) { - Log.Error("A null element was passed when attempting to save formatted as xml"); - throw new ArgumentNullException(nameof(element)); + throw new Exception("A null element was passed when attempting to save formatted as xml"); } element.Value = string.Empty; @@ -222,8 +216,7 @@ public static void AppendFormattedAsXml(XElement element, string valueToAppend, { if (element == null) { - Log.Error("A null element was passed when attempting to append formatted as xml"); - throw new ArgumentNullException(nameof(element)); + throw new Exception("A null element was passed when attempting to append formatted as xml"); } SaveFormattedAsXml(element, GetNodesInPlainText(element) + valueToAppend, removeUndesiredEndlines); @@ -233,14 +226,12 @@ public static void AddChildFormattedAsXml(XElement parent, XElement child, strin { if (parent == null) { - Log.Error("A null parent was passed when attempting to add child formatted as xml"); - throw new ArgumentNullException(nameof(parent)); + throw new Exception("A null parent was passed when attempting to add child formatted as xml"); } if (child == null) { - Log.Error("A null child was passed when attempting to add child formatted as xml"); - throw new ArgumentNullException(nameof(child)); + throw new Exception("A null child was passed when attempting to add child formatted as xml"); } SaveFormattedAsXml(child, childValue); diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs index d1a9554..0b188ca 100644 --- a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs +++ b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs @@ -14,6 +14,7 @@ public void Port_Basic() private void PortToTripleSlash( string testDataDir, bool save = true, + bool skipInterfaceImplementations = true, string assemblyName = TestData.TestAssembly, string namespaceName = TestData.TestNamespace, string typeName = TestData.TestType) @@ -31,7 +32,8 @@ private void PortToTripleSlash( { Direction = Configuration.PortingDirection.ToTripleSlash, CsProj = new FileInfo(testData.ProjectFilePath), - Save = save + Save = save, + SkipInterfaceImplementations = skipInterfaceImplementations }; c.IncludedAssemblies.Add(assemblyName); diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index d10f687..6eb82f8 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -5,8 +5,6 @@ namespace MyNamespace /// This is the MyType class summary. /// This is the MyProperty value. /// This is the MyField summary. /// This is the MyIntMethod return value. It mentions the . /// This is the MyVoidMethod summary. /// Date: Fri, 8 Jan 2021 20:27:27 -0800 Subject: [PATCH 22/65] Make some simplifications on remarks based on feedback. Fix nullability warnings. Obtain all projects related to all relevant symbols. --- Libraries/Configuration.cs | 1 - Libraries/Docs/DocsAPI.cs | 2 +- Libraries/Docs/DocsMember.cs | 10 +- .../IntelliSenseXmlCommentsContainer.cs | 1 - .../IntelliSenseXml/IntelliSenseXmlMember.cs | 8 +- .../TripleSlashSyntaxRewriter.cs | 15 +- Libraries/ToTripleSlashPorter.cs | 194 ++++++++++++------ Libraries/XmlHelper.cs | 4 +- Program/DocsPortingTool.cs | 4 +- .../TestData/Basic/SourceExpected.cs | 17 -- 10 files changed, 148 insertions(+), 108 deletions(-) diff --git a/Libraries/Configuration.cs b/Libraries/Configuration.cs index 9424909..f2843ac 100644 --- a/Libraries/Configuration.cs +++ b/Libraries/Configuration.cs @@ -160,7 +160,6 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) break; default: throw new Exception($"Unrecognized direction value: {arg}"); - break; } mode = Mode.Initial; break; diff --git a/Libraries/Docs/DocsAPI.cs b/Libraries/Docs/DocsAPI.cs index 64b2a32..f29246d 100644 --- a/Libraries/Docs/DocsAPI.cs +++ b/Libraries/Docs/DocsAPI.cs @@ -76,7 +76,7 @@ public XElement Docs { get { - return XERoot.Element("Docs"); + return XERoot.Element("Docs") ?? throw new NullReferenceException($"Docs section was null in {FilePath}"); } } diff --git a/Libraries/Docs/DocsMember.cs b/Libraries/Docs/DocsMember.cs index 4b919a3..e6345d7 100644 --- a/Libraries/Docs/DocsMember.cs +++ b/Libraries/Docs/DocsMember.cs @@ -83,12 +83,8 @@ public string ImplementsInterfaceMember { get { - XElement xeImplements = XERoot.Element("Implements"); - if (xeImplements != null) - { - return XmlHelper.GetChildElementValue(xeImplements, "InterfaceMember"); - } - return string.Empty; + XElement? xeImplements = XERoot.Element("Implements"); + return (xeImplements != null) ? XmlHelper.GetChildElementValue(xeImplements, "InterfaceMember") : string.Empty; } } @@ -96,7 +92,7 @@ public string ReturnType { get { - XElement xeReturnValue = XERoot.Element("ReturnValue"); + XElement? xeReturnValue = XERoot.Element("ReturnValue"); if (xeReturnValue != null) { return XmlHelper.GetChildElementValue(xeReturnValue, "ReturnType"); diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs index 7a9ce6b..e6b5a55 100644 --- a/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs @@ -88,7 +88,6 @@ private void LoadFile(FileInfo fileInfo, bool printSuccess) if (!fileInfo.Exists) { throw new Exception($"The IntelliSense xml file does not exist: {fileInfo.FullName}"); - return; } xDoc = XDocument.Load(fileInfo.FullName); diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs index c0c56e8..17157e7 100644 --- a/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs @@ -92,7 +92,7 @@ public string Summary { if (_summary == null) { - XElement xElement = XEMember.Element("summary"); + XElement? xElement = XEMember.Element("summary"); _summary = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; } return _summary; @@ -106,7 +106,7 @@ public string Value { if (_value == null) { - XElement xElement = XEMember.Element("value"); + XElement? xElement = XEMember.Element("value"); _value = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; } return _value; @@ -120,7 +120,7 @@ public string Returns { if (_returns == null) { - XElement xElement = XEMember.Element("returns"); + XElement? xElement = XEMember.Element("returns"); _returns = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; } return _returns; @@ -134,7 +134,7 @@ public string Remarks { if (_remarks == null) { - XElement xElement = XEMember.Element("remarks"); + XElement? xElement = XEMember.Element("remarks"); _remarks = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; } return _remarks; diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index cce596a..d8e8aa1 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -260,7 +260,7 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp if (!text.IsDocsEmpty()) { string trimmedRemarks = text.RemoveSubstrings("").Trim(); // The SyntaxFactory needs to be the one to add these - SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true, removeRemarksHeader: true); + SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true, remarks: true); XmlNodeSyntax xmlRemarksContent = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(xmlRemarksContent); @@ -416,7 +416,7 @@ private SyntaxTriviaList GetRelateds(List docsRelateds, SyntaxTrivi return relateds; } - private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace, bool addInitialNewLine, bool removeRemarksHeader = false) + private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace, bool addInitialNewLine, bool remarks = false) { string whitespace = leadingWhitespace.ToFullString().Replace(Environment.NewLine, ""); SyntaxToken newLineAndWhitespace = SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace); @@ -431,8 +431,6 @@ private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhi // Only add the initial new line and whitespace if the contents have more than one line. Otherwise, we want the contents to be inlined inside the tags. if (splittedLines.Length > 1 && addInitialNewLine) { - // For example, the remarks section needs a new line before the initial "## Remarks" title - tokens.Add(newLineAndWhitespace); tokens.Add(newLineAndWhitespace); } @@ -440,11 +438,11 @@ private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhi { string line = splittedLines[lineNumber]; - if (removeRemarksHeader && - (line.Contains("## Remarks") || line.Contains("##Remarks"))) + // Avoid adding the '## Remarks' header, it's unnecessary + if (remarks && (line.Contains("## Remarks") || line.Contains("##Remarks"))) { - // Avoid adding the '## Remarks' header, it's unnecessary - removeRemarksHeader = false; + // Reduces the number of Contains calls + remarks = false; continue; } @@ -455,7 +453,6 @@ private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhi if (splittedLines.Length > 1) { tokens.Add(newLineAndWhitespace); - tokens.Add(newLineAndWhitespace); } } return SyntaxFactory.TokenList(tokens); diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index 0f70755..8d5da3b 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -17,9 +17,50 @@ namespace Libraries { public class ToTripleSlashPorter { + private struct ProjectData + { + public MSBuildWorkspace Workspace; + public Project Project; + public Compilation Compilation; + } + + private struct SymbolData + { + public ProjectData ProjectData; + public DocsType Api; + } + private readonly Configuration Config; private readonly DocsCommentsContainer DocsComments; - private VisualStudioInstance MSBuildInstance; + private readonly VisualStudioInstance MSBuildInstance; + + private List ProjectDatas = new(); +#pragma warning disable RS1024 // Compare symbols correctly + // Bug fixed https://github.com/dotnet/roslyn-analyzers/pull/4571 + private Dictionary ResolvedSymbols = new(); +#pragma warning restore RS1024 // Compare symbols correctly + + BinaryLogger? _binLogger = null; + private BinaryLogger? BinLogger + { + get + { + if (Config.BinLogger) + { + if (_binLogger == null) + { + _binLogger = new BinaryLogger() + { + Parameters = Path.Combine(Environment.CurrentDirectory, Config.BinLogPath), + Verbosity = Microsoft.Build.Framework.LoggerVerbosity.Diagnostic, + CollectProjectImports = BinaryLogger.ProjectImportsCollectionMode.Embed + }; + } + } + + return _binLogger; + } + } public ToTripleSlashPorter(Configuration config) { @@ -27,6 +68,7 @@ public ToTripleSlashPorter(Configuration config) { throw new InvalidOperationException($"Unexpected porting direction: {config.Direction}"); } + Config = config; DocsComments = new DocsCommentsContainer(config); @@ -46,46 +88,83 @@ public void Start() Log.Info("Porting from Docs to triple slash..."); - MSBuildWorkspace workspace; - try - { - workspace = MSBuildWorkspace.Create(); - } - catch (ReflectionTypeLoadException) - { - throw new Exception("The MSBuild directory was not found in PATH. Use '-MSBuild ' to specify it."); - } - - CheckDiagnostics(workspace, "MSBuildWorkspace.Create"); + // Load and store the main project + ProjectDatas.Add(GetProjectData(Config.CsProj!.FullName)); - BinaryLogger? binLogger = null; - if (Config.BinLogger) + foreach (DocsType docsType in DocsComments.Types) { - binLogger = new BinaryLogger() + foreach (ProjectData pd in ProjectDatas) { - Parameters = Path.Combine(Environment.CurrentDirectory, Config.BinLogPath), - Verbosity = Microsoft.Build.Framework.LoggerVerbosity.Diagnostic, - CollectProjectImports = BinaryLogger.ProjectImportsCollectionMode.Embed - }; - } + // Try to find the symbol in the current compilation + INamedTypeSymbol? symbol = + pd.Compilation.GetTypeByMetadataName(docsType.FullName) ?? + pd.Compilation.Assembly.GetTypeByMetadataName(docsType.FullName); + + // If not found, nothing to do - It means that the Docs for APIs + // from an unrelated namespace were loaded for this compilation's assembly + if (symbol == null) + { + Log.Warning($"Type symbol not found in compilation: {docsType.DocId}."); + continue; + } - Project? project = workspace.OpenProjectAsync(Config.CsProj!.FullName, msbuildLogger: binLogger).Result; - if (project == null) - { - throw new Exception("Could not find a project."); + // Make sure at least one syntax tree of this symbol can be found in the current project's compilation + // Otherwise, retrieve the correct project where this symbol is supposed to be found + + Location location = symbol.Locations.FirstOrDefault() + ?? throw new NullReferenceException($"No locations found for {docsType.FullName}."); + + SyntaxTree tree = location.SourceTree + ?? throw new NullReferenceException($"No tree found in the location of {docsType.FullName}."); + + if (pd.Compilation.SyntaxTrees.FirstOrDefault(x => x.FilePath == tree.FilePath) is null) + { + // The symbol has to live in one of the current project's referenced projects + foreach (ProjectReference projectReference in pd.Project.ProjectReferences) + { + PropertyInfo prop = typeof(ProjectId).GetProperty("DebugName", BindingFlags.NonPublic | BindingFlags.Instance) + ?? throw new NullReferenceException("ProjectId.DebugName private property not found."); + + string projectPath = prop.GetValue(projectReference.ProjectId)?.ToString() + ?? throw new NullReferenceException("ProjectId.DebugName value was null."); + + if (string.IsNullOrWhiteSpace(projectPath)) + { + throw new Exception("Project path was empty."); + } + + // Can't reuse the existing Workspace or exception thrown saying we already have the project loaded in this workspace. + // Unfortunately, there is no way to retrieve a references project as a Project instance from the existing workspace. + ProjectData pd2 = GetProjectData(projectPath); + ProjectDatas.Add(pd2); + ResolvedSymbols.Add(symbol, new SymbolData { Api = docsType, ProjectData = pd2 }); + } + } + else + { + ResolvedSymbols.Add(symbol, new SymbolData { Api = docsType, ProjectData = pd }); + } + } } - CheckDiagnostics(workspace, "workspace.OpenProjectAsync"); - Compilation? compilation = project.GetCompilationAsync().Result; - if (compilation == null) + foreach ((ISymbol symbol, SymbolData data) in ResolvedSymbols) { - throw new NullReferenceException("The project's compilation was null."); - } + ProjectData t = data.ProjectData; + foreach (Location location in symbol.Locations) + { + SyntaxTree tree = location.SourceTree + ?? throw new NullReferenceException($"Tree null for {data.Api.FullName}"); - CheckDiagnostics(workspace, "project.GetCompilationAsync"); + SemanticModel model = t.Compilation.GetSemanticModel(tree); + TripleSlashSyntaxRewriter rewriter = new(DocsComments, model, location, location.SourceTree); + SyntaxNode newRoot = rewriter.Visit(tree.GetRoot()) + ?? throw new NullReferenceException($"Returned null root node for {data.Api.FullName} in {tree.FilePath}"); + + File.WriteAllText(tree.FilePath, newRoot.ToFullString()); + } + } - PortCommentsForProject(compilation!); } private void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) @@ -116,46 +195,33 @@ private void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) } } - private void PortCommentsForProject(Compilation compilation) + private ProjectData GetProjectData(string csprojPath) { - foreach (DocsType docsType in DocsComments.Types) + ProjectData t = new ProjectData(); + + try { - INamedTypeSymbol? typeSymbol = - compilation.GetTypeByMetadataName(docsType.FullName) ?? - compilation.Assembly.GetTypeByMetadataName(docsType.FullName); + t.Workspace = MSBuildWorkspace.Create(); + } + catch (ReflectionTypeLoadException) + { + Log.Error("The MSBuild directory was not found in PATH. Use '-MSBuild ' to specify it."); + throw; + } - if (typeSymbol == null) - { - Log.Warning($"Type symbol not found in compilation: {docsType.DocId}"); - continue; - } + CheckDiagnostics(t.Workspace, "MSBuildWorkspace.Create"); - PortCommentsForType(compilation, docsType, typeSymbol); - } - } + t.Project = t.Workspace.OpenProjectAsync(csprojPath, msbuildLogger: BinLogger).Result + ?? throw new NullReferenceException($"Could not find the project: {csprojPath}"); - private void PortCommentsForType(Compilation compilation, IDocsAPI api, ISymbol symbol) - { - foreach (Location location in symbol.Locations) - { - SyntaxTree? tree = location.SourceTree; - if (tree == null) - { - Log.Warning($"Tree not found for location of {symbol.Name}"); - continue; - } + CheckDiagnostics(t.Workspace, $"workspace.OpenProjectAsync - {csprojPath}"); - SemanticModel model = compilation.GetSemanticModel(tree); - var rewriter = new TripleSlashSyntaxRewriter(DocsComments, model, location, tree); - SyntaxNode? newRoot = rewriter.Visit(tree.GetRoot()); - if (newRoot == null) - { - Log.Warning($"New returned root is null for {api.DocId} in {tree.FilePath}"); - continue; - } + t.Compilation = t.Project.GetCompilationAsync().Result + ?? throw new NullReferenceException("The project's compilation was null."); - File.WriteAllText(tree.FilePath, newRoot.ToFullString()); - } + CheckDiagnostics(t.Workspace, $"project.GetCompilationAsync - {csprojPath}"); + + return t; } #region MSBuild loading logic diff --git a/Libraries/XmlHelper.cs b/Libraries/XmlHelper.cs index 198e667..8f33d27 100644 --- a/Libraries/XmlHelper.cs +++ b/Libraries/XmlHelper.cs @@ -91,7 +91,7 @@ public static string GetAttributeValue(XElement parent, string name) } else { - XAttribute attr = parent.Attribute(name); + XAttribute? attr = parent.Attribute(name); if (attr != null) { return attr.Value.Trim(); @@ -114,7 +114,7 @@ public static bool TryGetChildElement(XElement parent, string name, out XElement public static string GetChildElementValue(XElement parent, string childName) { - XElement child = parent.Element(childName); + XElement? child = parent.Element(childName); if (child != null) { diff --git a/Program/DocsPortingTool.cs b/Program/DocsPortingTool.cs index 7aae3f8..3205900 100644 --- a/Program/DocsPortingTool.cs +++ b/Program/DocsPortingTool.cs @@ -13,13 +13,13 @@ public static void Main(string[] args) { case Configuration.PortingDirection.ToDocs: { - var porter = new ToDocsPorter(config); + ToDocsPorter porter = new(config); porter.Start(); break; } case Configuration.PortingDirection.ToTripleSlash: { - var porter = new ToTripleSlashPorter(config); + ToTripleSlashPorter porter = new(config); porter.Start(); break; } diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index 6eb82f8..d8348c5 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -4,11 +4,8 @@ namespace MyNamespace { /// This is the MyType class summary. /// public class MyType { @@ -27,11 +24,8 @@ internal MyType(int myProperty) /// This is the MyProperty summary. /// This is the MyProperty value. /// public int MyProperty { @@ -41,11 +35,8 @@ public int MyProperty /// This is the MyField summary. /// public int MyField = 1; @@ -54,13 +45,9 @@ public int MyProperty /// This is the MyIntMethod param2 summary. /// This is the MyIntMethod return value. It mentions the . /// . - /// /// ]]> /// This is the ArgumentNullException thrown by MyIntMethod. It mentions the . /// This is the IndexOutOfRangeException thrown by MyIntMethod. @@ -71,13 +58,9 @@ public int MyIntMethod(int param1, int param2) /// This is the MyVoidMethod summary. /// . - /// /// ]]> /// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . /// This is the IndexOutOfRangeException thrown by MyVoidMethod. From 06ff96eca463abf923e8b5761093a54ad4bb0e32 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Sat, 9 Jan 2021 17:35:32 -0800 Subject: [PATCH 23/65] Fix calling the VSInstance loading code before calling the porter constructor. Avoid adding CDATA to remarks. --- .../TripleSlashSyntaxRewriter.cs | 10 +- Libraries/ToTripleSlashPorter.cs | 130 +++++++++--------- Program/DocsPortingTool.cs | 3 +- .../PortToTripleSlashTests.cs | 19 ++- 4 files changed, 86 insertions(+), 76 deletions(-) diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index d8e8aa1..94033fd 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -15,7 +15,7 @@ internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter private DocsCommentsContainer DocsComments { get; } private SemanticModel Model { get; } - public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticModel model, Location location, SyntaxTree tree) : base(visitIntoStructuredTrivia: true) + public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticModel model) : base(visitIntoStructuredTrivia: true) { DocsComments = docsComments; Model = model; @@ -260,10 +260,10 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp if (!text.IsDocsEmpty()) { string trimmedRemarks = text.RemoveSubstrings("").Trim(); // The SyntaxFactory needs to be the one to add these - SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true, remarks: true); - XmlNodeSyntax xmlRemarksContent = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); - XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(xmlRemarksContent); - + //SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true, remarks: true); + //XmlNodeSyntax contents = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); + SyntaxList contents = GetContentsInRows(trimmedRemarks); + XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(contents); return GetXmlTrivia(xmlRemarks, leadingWhitespace); } diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index 8d5da3b..a6be51c 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -32,12 +32,10 @@ private struct SymbolData private readonly Configuration Config; private readonly DocsCommentsContainer DocsComments; - private readonly VisualStudioInstance MSBuildInstance; - private List ProjectDatas = new(); #pragma warning disable RS1024 // Compare symbols correctly // Bug fixed https://github.com/dotnet/roslyn-analyzers/pull/4571 - private Dictionary ResolvedSymbols = new(); + private readonly Dictionary ResolvedSymbols = new(); #pragma warning restore RS1024 // Compare symbols correctly BinaryLogger? _binLogger = null; @@ -62,7 +60,7 @@ private BinaryLogger? BinLogger } } - public ToTripleSlashPorter(Configuration config) + private ToTripleSlashPorter(Configuration config) { if (config.Direction != Configuration.PortingDirection.ToTripleSlash) { @@ -71,14 +69,18 @@ public ToTripleSlashPorter(Configuration config) Config = config; DocsComments = new DocsCommentsContainer(config); + } + + public static void Start(Configuration config) + { + // IMPORTANT: Need to load the MSBuild property before calling the ToTripleSlashPorter constructor. + LoadVSInstance(); - // This ensures we can load MSBuild property before calling the ToTripleSlashPorter constructor - MSBuildInstance = MSBuildLocator.QueryVisualStudioInstances().First(); - Register(MSBuildInstance.MSBuildPath); - MSBuildLocator.RegisterInstance(MSBuildInstance); + ToTripleSlashPorter porter = new ToTripleSlashPorter(config); + porter.Port(); } - public void Start() + private void Port() { DocsComments.CollectFiles(); if (!DocsComments.Types.Any()) @@ -89,62 +91,58 @@ public void Start() Log.Info("Porting from Docs to triple slash..."); // Load and store the main project - ProjectDatas.Add(GetProjectData(Config.CsProj!.FullName)); + ProjectData mainProjectData = GetProjectData(Config.CsProj!.FullName); foreach (DocsType docsType in DocsComments.Types) { - foreach (ProjectData pd in ProjectDatas) + // Try to find the symbol in the current compilation + INamedTypeSymbol? symbol = + mainProjectData.Compilation.GetTypeByMetadataName(docsType.FullName) ?? + mainProjectData.Compilation.Assembly.GetTypeByMetadataName(docsType.FullName); + + // If not found, nothing to do - It means that the Docs for APIs + // from an unrelated namespace were loaded for this compilation's assembly + if (symbol == null) { - // Try to find the symbol in the current compilation - INamedTypeSymbol? symbol = - pd.Compilation.GetTypeByMetadataName(docsType.FullName) ?? - pd.Compilation.Assembly.GetTypeByMetadataName(docsType.FullName); - - // If not found, nothing to do - It means that the Docs for APIs - // from an unrelated namespace were loaded for this compilation's assembly - if (symbol == null) - { - Log.Warning($"Type symbol not found in compilation: {docsType.DocId}."); - continue; - } + Log.Warning($"Type symbol not found in compilation: {docsType.DocId}."); + continue; + } - // Make sure at least one syntax tree of this symbol can be found in the current project's compilation - // Otherwise, retrieve the correct project where this symbol is supposed to be found - - Location location = symbol.Locations.FirstOrDefault() - ?? throw new NullReferenceException($"No locations found for {docsType.FullName}."); + // Make sure at least one syntax tree of this symbol can be found in the current project's compilation + // Otherwise, retrieve the correct project where this symbol is supposed to be found - SyntaxTree tree = location.SourceTree - ?? throw new NullReferenceException($"No tree found in the location of {docsType.FullName}."); + Location location = symbol.Locations.FirstOrDefault() + ?? throw new NullReferenceException($"No locations found for {docsType.FullName}."); + + SyntaxTree tree = location.SourceTree + ?? throw new NullReferenceException($"No tree found in the location of {docsType.FullName}."); - if (pd.Compilation.SyntaxTrees.FirstOrDefault(x => x.FilePath == tree.FilePath) is null) + if (mainProjectData.Compilation.SyntaxTrees.FirstOrDefault(x => x.FilePath == tree.FilePath) is null) + { + // The symbol has to live in one of the current project's referenced projects + foreach (ProjectReference projectReference in mainProjectData.Project.ProjectReferences) { - // The symbol has to live in one of the current project's referenced projects - foreach (ProjectReference projectReference in pd.Project.ProjectReferences) + PropertyInfo prop = typeof(ProjectId).GetProperty("DebugName", BindingFlags.NonPublic | BindingFlags.Instance) + ?? throw new NullReferenceException("ProjectId.DebugName private property not found."); + + string projectPath = prop.GetValue(projectReference.ProjectId)?.ToString() + ?? throw new NullReferenceException("ProjectId.DebugName value was null."); + + if (string.IsNullOrWhiteSpace(projectPath)) { - PropertyInfo prop = typeof(ProjectId).GetProperty("DebugName", BindingFlags.NonPublic | BindingFlags.Instance) - ?? throw new NullReferenceException("ProjectId.DebugName private property not found."); - - string projectPath = prop.GetValue(projectReference.ProjectId)?.ToString() - ?? throw new NullReferenceException("ProjectId.DebugName value was null."); - - if (string.IsNullOrWhiteSpace(projectPath)) - { - throw new Exception("Project path was empty."); - } - - // Can't reuse the existing Workspace or exception thrown saying we already have the project loaded in this workspace. - // Unfortunately, there is no way to retrieve a references project as a Project instance from the existing workspace. - ProjectData pd2 = GetProjectData(projectPath); - ProjectDatas.Add(pd2); - ResolvedSymbols.Add(symbol, new SymbolData { Api = docsType, ProjectData = pd2 }); + throw new Exception("Project path was empty."); } - } - else - { - ResolvedSymbols.Add(symbol, new SymbolData { Api = docsType, ProjectData = pd }); + + // Can't reuse the existing Workspace or exception thrown saying we already have the project loaded in this workspace. + // Unfortunately, there is no way to retrieve a references project as a Project instance from the existing workspace. + ProjectData extraProjectData = GetProjectData(projectPath); + ResolvedSymbols.Add(symbol, new SymbolData { Api = docsType, ProjectData = extraProjectData }); } } + else + { + ResolvedSymbols.Add(symbol, new SymbolData { Api = docsType, ProjectData = mainProjectData }); + } } @@ -157,14 +155,13 @@ public void Start() ?? throw new NullReferenceException($"Tree null for {data.Api.FullName}"); SemanticModel model = t.Compilation.GetSemanticModel(tree); - TripleSlashSyntaxRewriter rewriter = new(DocsComments, model, location, location.SourceTree); + TripleSlashSyntaxRewriter rewriter = new(DocsComments, model); SyntaxNode newRoot = rewriter.Visit(tree.GetRoot()) ?? throw new NullReferenceException($"Returned null root node for {data.Api.FullName} in {tree.FilePath}"); File.WriteAllText(tree.FilePath, newRoot.ToFullString()); } } - } private void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) @@ -197,7 +194,7 @@ private void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) private ProjectData GetProjectData(string csprojPath) { - ProjectData t = new ProjectData(); + ProjectData t = new(); try { @@ -224,16 +221,24 @@ private ProjectData GetProjectData(string csprojPath) return t; } + #region MSBuild loading logic - private static readonly Dictionary s_pathsToAssemblies = new Dictionary(StringComparer.OrdinalIgnoreCase); - private static readonly Dictionary s_namesToAssemblies = new Dictionary(); + private static readonly Dictionary s_pathsToAssemblies = new(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary s_namesToAssemblies = new(); - private static readonly object s_guard = new object(); + private static readonly object s_guard = new(); - /// - /// Register an assembly loader that will load assemblies with higher version than what was requested. - /// + // Loads the external VS instance using the correct MSBuild dependency, which differs from the one used by this process. + public static VisualStudioInstance LoadVSInstance() + { + VisualStudioInstance vsBuildInstance = MSBuildLocator.QueryVisualStudioInstances().First(); + Register(vsBuildInstance.MSBuildPath); + MSBuildLocator.RegisterInstance(vsBuildInstance); + return vsBuildInstance; + } + + // Register an assembly loader that will load assemblies with higher version than what was requested. private static void Register(string searchPath) { AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext context, AssemblyName assemblyName) => @@ -314,6 +319,5 @@ private static void Register(string searchPath) } #endregion - } } diff --git a/Program/DocsPortingTool.cs b/Program/DocsPortingTool.cs index 3205900..828b39b 100644 --- a/Program/DocsPortingTool.cs +++ b/Program/DocsPortingTool.cs @@ -19,8 +19,7 @@ public static void Main(string[] args) } case Configuration.PortingDirection.ToTripleSlash: { - ToTripleSlashPorter porter = new(config); - porter.Start(); + ToTripleSlashPorter.Start(config); break; } default: diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs index 0b188ca..884af43 100644 --- a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs +++ b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs @@ -1,4 +1,11 @@ -using System.IO; +#nullable enable +using Microsoft.Build.Locator; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; using Xunit; namespace Libraries.Tests @@ -11,7 +18,7 @@ public void Port_Basic() PortToTripleSlash("Basic"); } - private void PortToTripleSlash( + private static void PortToTripleSlash( string testDataDir, bool save = true, bool skipInterfaceImplementations = true, @@ -19,9 +26,9 @@ private void PortToTripleSlash( string namespaceName = TestData.TestNamespace, string typeName = TestData.TestType) { - using TestDirectory tempDir = new TestDirectory(); + using TestDirectory tempDir = new(); - PortToTripleSlashTestData testData = new PortToTripleSlashTestData( + PortToTripleSlashTestData testData = new( tempDir, testDataDir, assemblyName: assemblyName, @@ -45,13 +52,13 @@ private void PortToTripleSlash( c.DirsDocsXml.Add(testData.DocsDir); - var porter = new ToTripleSlashPorter(c); + var porter = new ToTripleSlashPorter(c, ToTripleSlashPorter.LoadVSInstance()); porter.Start(); Verify(testData); } - private void Verify(PortToTripleSlashTestData testData) + private static void Verify(PortToTripleSlashTestData testData) { string[] expectedLines = File.ReadAllLines(testData.ExpectedFilePath); string[] actualLines = File.ReadAllLines(testData.ActualFilePath); From faede76d0170ec10e51fab1b731e3b550360ac01 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 15 Jan 2021 10:08:26 -0800 Subject: [PATCH 24/65] Address test PR suggestions. --- Libraries/Extensions.cs | 6 +- .../TripleSlashSyntaxRewriter.cs | 399 +++++++++++++----- Libraries/ToTripleSlashPorter.cs | 58 ++- Program/DocsPortingTool.csproj | 1 + Program/Properties/launchSettings.json | 4 +- .../PortToTripleSlashTests.cs | 3 +- .../TestData/Basic/MyDelegate.xml | 12 +- .../TestData/Basic/MyType.xml | 76 ++-- .../TestData/Basic/SourceExpected.cs | 82 ++-- .../TestData/Basic/SourceOriginal.cs | 44 +- 10 files changed, 475 insertions(+), 210 deletions(-) diff --git a/Libraries/Extensions.cs b/Libraries/Extensions.cs index 56c6a7b..40a5d63 100644 --- a/Libraries/Extensions.cs +++ b/Libraries/Extensions.cs @@ -39,7 +39,7 @@ public static string RemoveSubstrings(this string oldString, params string[] str public static bool IsDocsEmpty(this string? s) => string.IsNullOrWhiteSpace(s) || s == Configuration.ToBeAdded; - public static string WithoutPrefix(this string text) + public static string WithoutDocIdPrefixes(this string text) { if (text.Length > 2 && text[1] == ':') { @@ -48,8 +48,8 @@ public static string WithoutPrefix(this string text) return Regex.Replace( input: text, - pattern: @"(?.*)(?cref=""[A-Z]\:)(?.*)", - replacement: "${left}cref=\"${right}"); + pattern: @"cref=""[a-zA-Z]{1}\:(?[a-zA-Z0-9\._]+)""", + replacement: "cref=\"${cref}\""); } } diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index 94033fd..86d59c2 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -7,9 +7,85 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text.RegularExpressions; namespace Libraries.RoslynTripleSlash { + /* + The following triple slash comments section: + + /// + /// My summary. + /// + /// My param description. + /// My remarks. + public ... + + translates to this syntax tree structure: + + 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. + + 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 + + XmlElement -> My remarks. + XmlText -> new line char after + XmlTextLiteralNewLineToken -> new line char after + EndOfDocumentationCommentToken (SyntaxToken) -> invisible + + Lead: WhitespaceTrivia -> The 4 whitespace chars before the public keyword. + Trail: WhitespaceTrivia -> The single whitespace char after the public keyword. + */ internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter { private DocsCommentsContainer DocsComments { get; } @@ -83,6 +159,9 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) => VisitBaseMethodDeclaration(node); + public override SyntaxNode? VisitOperatorDeclaration(OperatorDeclarationSyntax node) => + VisitBaseMethodDeclaration(node); + public override SyntaxNode? VisitPropertyDeclaration(PropertyDeclarationSyntax node) { if (!TryGetMember(node, out DocsMember? member)) @@ -92,9 +171,9 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); - SyntaxTriviaList value = GetValue(member.Value, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member, leadingWhitespace); + SyntaxTriviaList value = GetValue(member, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member, leadingWhitespace); SyntaxTriviaList exceptions = GetExceptions(member.Exceptions, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); @@ -103,6 +182,20 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod return GetNodeWithTrivia(leadingWhitespace, node, summary, value, remarks, exceptions, seealsos, altmembers, relateds); } + public override SyntaxNode? VisitRecordDeclaration(RecordDeclarationSyntax node) + { + SyntaxNode? baseNode = base.VisitRecordDeclaration(node); + + ISymbol? symbol = Model.GetDeclaredSymbol(node); + if (symbol == null) + { + Log.Warning($"Symbol is null."); + return baseNode; + } + + return VisitType(baseNode, symbol); + } + public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node) { SyntaxNode? baseNode = base.VisitStructDeclaration(node); @@ -141,8 +234,8 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod } - SyntaxTriviaList summary = GetSummary(type.Summary, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(type.Remarks, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(type, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(type, leadingWhitespace); SyntaxTriviaList parameters = GetParameters(type, leadingWhitespace); SyntaxTriviaList typeParameters = GetTypeParameters(type, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(type.SeeAlsoCrefs, leadingWhitespace); @@ -164,11 +257,11 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member, leadingWhitespace); SyntaxTriviaList parameters = GetParameters(member, leadingWhitespace); SyntaxTriviaList typeParameters = GetTypeParameters(member, leadingWhitespace); - SyntaxTriviaList returns = GetReturns(member.Returns, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList returns = GetReturns(member, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member, leadingWhitespace); SyntaxTriviaList exceptions = GetExceptions(member.Exceptions, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); @@ -186,8 +279,8 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member, leadingWhitespace); SyntaxTriviaList exceptions = GetExceptions(member.Exceptions, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); @@ -211,8 +304,8 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace); @@ -223,46 +316,69 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod return node; } - private SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxNode node, params SyntaxTriviaList[] trivias) + private static SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxNode node, params SyntaxTriviaList[] trivias) { SyntaxTriviaList finalTrivia = new(); - var leadingTrivia = node.GetLeadingTrivia(); - if (leadingTrivia.Any()) + foreach (SyntaxTriviaList t in trivias) + { + finalTrivia = finalTrivia.AddRange(t); + } + if (finalTrivia.Count > 0) { - if (leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) + finalTrivia = finalTrivia.AddRange(leadingWhitespace); + + var leadingTrivia = node.GetLeadingTrivia(); + if (leadingTrivia.Any()) { - // Ensure the endline that separates nodes is respected - finalTrivia = new(SyntaxFactory.ElasticCarriageReturnLineFeed); + if (leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) + { + // Ensure the endline that separates nodes is respected + finalTrivia = new SyntaxTriviaList(SyntaxFactory.ElasticCarriageReturnLineFeed) + .AddRange(finalTrivia); + } } - } - foreach (SyntaxTriviaList t in trivias) - { - finalTrivia = finalTrivia.AddRange(t); + return node.WithLeadingTrivia(finalTrivia); } - finalTrivia = finalTrivia.AddRange(leadingWhitespace); - return node.WithLeadingTrivia(finalTrivia); + // If there was no new trivia, return untouched + return node; } - private SyntaxTriviaList GetLeadingWhitespace(SyntaxNode node) => - node.GetLeadingTrivia().Where(t => t.IsKind(SyntaxKind.WhitespaceTrivia)).ToSyntaxTriviaList(); + // 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) + { + if (node is MemberDeclarationSyntax memberDeclaration) + { + if (memberDeclaration.Modifiers.FirstOrDefault(x => x.IsKind(SyntaxKind.PublicKeyword) || x.IsKind(SyntaxKind.ProtectedKeyword)) is SyntaxToken publicModifier) + { + if (publicModifier.LeadingTrivia.LastOrDefault(t => t.IsKind(SyntaxKind.WhitespaceTrivia)) is SyntaxTrivia last) + { + return new(last); + } + } + } + return new(); + } - private SyntaxTriviaList GetSummary(string text, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetSummary(DocsAPI api, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); - XmlElementSyntax element = SyntaxFactory.XmlSummaryElement(contents); - return GetXmlTrivia(element, leadingWhitespace); + if (!api.Summary.IsDocsEmpty()) + { + XmlTextSyntax contents = GetTextAsCommentedTokens(api.Summary, leadingWhitespace); + XmlElementSyntax element = SyntaxFactory.XmlSummaryElement(contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + return new(); } - private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetRemarks(DocsAPI api, SyntaxTriviaList leadingWhitespace) { - if (!text.IsDocsEmpty()) + if (!api.Remarks.IsDocsEmpty()) { - string trimmedRemarks = text.RemoveSubstrings("").Trim(); // The SyntaxFactory needs to be the one to add these - //SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true, remarks: true); - //XmlNodeSyntax contents = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); - SyntaxList contents = GetContentsInRows(trimmedRemarks); + string text = GetRemarksWithXmlParameters(api); + XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace, markdown: true); XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(contents); return GetXmlTrivia(xmlRemarks, leadingWhitespace); } @@ -270,74 +386,123 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp return new(); } - private SyntaxTriviaList GetValue(string text, SyntaxTriviaList leadingWhitespace) + private static string GetRemarksWithXmlParameters(IDocsAPI api) { - SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); - XmlElementSyntax element = SyntaxFactory.XmlValueElement(contents); - return GetXmlTrivia(element, leadingWhitespace); + string remarks = api.Remarks; + + if (!api.Remarks.IsDocsEmpty() && ( + api.Params.Any() || api.TypeParams.Any())) + { + MatchCollection collection = Regex.Matches(api.Remarks, @"(?`(?[a-zA-Z0-9_]+)`)"); + + foreach (Match match in collection) + { + string backtickedParam = match.Groups["backtickedParam"].Value; + string paramName = match.Groups["paramName"].Value; + if (api.Params.Any(x => x.Name == paramName)) + { + remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); + } + else if (api.TypeParams.Any(x => x.Name == paramName)) + { + remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); + } + } + } + + return remarks; } - private SyntaxTriviaList GetParameter(string name, string text, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetValue(DocsMember api, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); - XmlElementSyntax element = SyntaxFactory.XmlParamElement(name, contents); - return GetXmlTrivia(element, leadingWhitespace); + if (!api.Value.IsDocsEmpty()) + { + XmlTextSyntax contents = GetTextAsCommentedTokens(api.Value, leadingWhitespace); + XmlElementSyntax element = SyntaxFactory.XmlValueElement(contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + return new(); + } + + 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(element, leadingWhitespace); + } + + return new(); } - private SyntaxTriviaList GetParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList parameters = new(); - foreach (SyntaxTriviaList parameterTrivia in api.Params.Select( - param => GetParameter(param.Name, param.Value, leadingWhitespace))) + 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 SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace) { - var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute("name", name)); - SyntaxList contents = GetContentsInRows(text); - return GetXmlTrivia("typeparam", attribute, contents, 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 SyntaxTriviaList GetTypeParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetTypeParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList typeParameters = new(); - foreach (SyntaxTriviaList typeParameterTrivia in api.TypeParams.Select( - typeParam => GetTypeParam(typeParam.Name, typeParam.Value, leadingWhitespace))) + 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 SyntaxTriviaList GetReturns(string text, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetReturns(DocsMember api, SyntaxTriviaList leadingWhitespace) { - // For when returns is empty because the method returns void - if (string.IsNullOrWhiteSpace(text)) + // Also applies for when is empty because the method return type is void + if (!api.Returns.IsDocsEmpty()) { - return new(); + XmlTextSyntax contents = GetTextAsCommentedTokens(api.Returns, leadingWhitespace); + XmlElementSyntax element = SyntaxFactory.XmlReturnsElement(contents); + return GetXmlTrivia(element, leadingWhitespace); } - SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); - XmlElementSyntax element = SyntaxFactory.XmlReturnsElement(contents); - return GetXmlTrivia(element, leadingWhitespace); + return new(); } - private SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace) { - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutPrefix())); - XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text.WithoutPrefix(), leadingWhitespace, addInitialNewLine: false)); - XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); - return GetXmlTrivia(element, leadingWhitespace); + if (!text.IsDocsEmpty()) + { + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutDocIdPrefixes())); + //XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text.WithoutPrefix(), leadingWhitespace)); + XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace); + XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + return new(); } - private SyntaxTriviaList GetExceptions(List docsExceptions, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetExceptions(List docsExceptions, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList exceptions = new(); - // No need to add exceptions in secondary files if (docsExceptions.Any()) { foreach (SyntaxTriviaList exceptionsTrivia in docsExceptions.Select( @@ -349,14 +514,14 @@ private SyntaxTriviaList GetExceptions(List docsExceptions, Synta return exceptions; } - private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace) { - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutPrefix())); + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutDocIdPrefixes())); XmlEmptyElementSyntax element = SyntaxFactory.XmlSeeAlsoElement(crefSyntax); return GetXmlTrivia(element, leadingWhitespace); } - private SyntaxTriviaList GetSeeAlsos(List docsSeeAlsoCrefs, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetSeeAlsos(List docsSeeAlsoCrefs, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList seealsos = new(); if (docsSeeAlsoCrefs.Any()) @@ -370,14 +535,14 @@ private SyntaxTriviaList GetSeeAlsos(List docsSeeAlsoCrefs, SyntaxTrivia return seealsos; } - private SyntaxTriviaList GetAltMember(string cref, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetAltMember(string cref, SyntaxTriviaList leadingWhitespace) { - XmlAttributeSyntax attribute = SyntaxFactory.XmlTextAttribute("cref", cref.WithoutPrefix()); + XmlAttributeSyntax attribute = SyntaxFactory.XmlTextAttribute("cref", cref.WithoutDocIdPrefixes()); XmlEmptyElementSyntax emptyElement = SyntaxFactory.XmlEmptyElement(SyntaxFactory.XmlName(SyntaxFactory.Identifier("altmember")), new SyntaxList(attribute)); return GetXmlTrivia(emptyElement, leadingWhitespace); } - private SyntaxTriviaList GetAltMembers(List docsAltMembers, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetAltMembers(List docsAltMembers, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList altMembers = new(); if (docsAltMembers.Any()) @@ -391,18 +556,18 @@ private SyntaxTriviaList GetAltMembers(List docsAltMembers, SyntaxTrivia return altMembers; } - private SyntaxTriviaList GetRelated(string articleType, string href, string value, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetRelated(string articleType, string href, string value, SyntaxTriviaList leadingWhitespace) { SyntaxList attributes = new(); attributes = attributes.Add(SyntaxFactory.XmlTextAttribute("type", articleType)); attributes = attributes.Add(SyntaxFactory.XmlTextAttribute("href", href)); - SyntaxList contents = GetContentsInRows(value); + XmlTextSyntax contents = GetTextAsCommentedTokens(value, leadingWhitespace); return GetXmlTrivia("related", attributes, contents, leadingWhitespace); } - private SyntaxTriviaList GetRelateds(List docsRelateds, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetRelateds(List docsRelateds, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList relateds = new(); if (docsRelateds.Any()) @@ -416,61 +581,62 @@ private SyntaxTriviaList GetRelateds(List docsRelateds, SyntaxTrivi return relateds; } - private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace, bool addInitialNewLine, bool remarks = false) + private static string ReplaceText(string text, bool markdown) + { + if (markdown) + { + text = Regex.Replace(text, @"", ""); + text = Regex.Replace(text, @"##[ ]?Remarks(\r?\n)*[\t ]*", ""); + text = Regex.Replace(text, @"(?[a-zA-Z0-9_\.]+)>)", ""); + } + else + { + text = text.WithoutDocIdPrefixes(); + } + + return text; + } + + /* + XmlText + XmlTextLiteralNewLineToken (XmlTextSyntax) -> endline + XmlTextLiteralToken (XmlTextLiteralToken) -> [ text] + Lead: DocumentationCommentExteriorTrivia (SyntaxTrivia) -> [ /// ] + */ + private static XmlTextSyntax GetTextAsCommentedTokens(string text, SyntaxTriviaList leadingWhitespace, bool markdown = false) { - string whitespace = leadingWhitespace.ToFullString().Replace(Environment.NewLine, ""); - SyntaxToken newLineAndWhitespace = SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace); + text = ReplaceText(text, markdown); + + // collapse newlines to a single one + string whitespace = Regex.Replace(leadingWhitespace.ToFullString(), @"(\r?\n)+", ""); + SyntaxToken whitespaceToken = SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace); SyntaxTrivia leadingTrivia = SyntaxFactory.SyntaxTrivia(SyntaxKind.DocumentationCommentExteriorTrivia, string.Empty); SyntaxTriviaList leading = SyntaxTriviaList.Create(leadingTrivia); - + var tokens = new List(); - string[] splittedLines = text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + string[] lines = text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - // Only add the initial new line and whitespace if the contents have more than one line. Otherwise, we want the contents to be inlined inside the tags. - if (splittedLines.Length > 1 && addInitialNewLine) + for (int lineNumber = 0; lineNumber < lines.Length; lineNumber++) { - tokens.Add(newLineAndWhitespace); - } - - for (int lineNumber = 0; lineNumber < splittedLines.Length; lineNumber++) - { - string line = splittedLines[lineNumber]; - - // Avoid adding the '## Remarks' header, it's unnecessary - if (remarks && (line.Contains("## Remarks") || line.Contains("##Remarks"))) - { - // Reduces the number of Contains calls - remarks = false; - continue; - } + string line = lines[lineNumber]; SyntaxToken token = SyntaxFactory.XmlTextLiteral(leading, line, line, default); tokens.Add(token); - // Only add extra new lines if we expect more than one line of text in the contents. Otherwise, inline it inside the tags. - if (splittedLines.Length > 1) + if (lines.Length > 1 && lineNumber < lines.Length - 1) { - tokens.Add(newLineAndWhitespace); + tokens.Add(whitespaceToken); } } - return SyntaxFactory.TokenList(tokens); - } - private SyntaxList GetContentsInRows(string text) - { - var nodes = new SyntaxList(); - foreach (string line in text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) - { - var tokenList = SyntaxFactory.ParseTokens(line).ToArray(); // Prevents unexpected change from "<" to "<" - XmlTextSyntax xmlText = SyntaxFactory.XmlText(tokenList); - return nodes.Add(xmlText); - } - return nodes; + XmlTextSyntax xmlText = SyntaxFactory.XmlText(tokens.ToArray()); + return xmlText; } - private SyntaxTriviaList GetXmlTrivia(XmlNodeSyntax node, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetXmlTrivia(XmlNodeSyntax node, SyntaxTriviaList leadingWhitespace) { DocumentationCommentTriviaSyntax docComment = SyntaxFactory.DocumentationComment(node); SyntaxTrivia docCommentTrivia = SyntaxFactory.Trivia(docComment); @@ -483,7 +649,7 @@ private SyntaxTriviaList GetXmlTrivia(XmlNodeSyntax node, SyntaxTriviaList leadi // Generates a custom SyntaxTrivia object containing a triple slashed xml element with optional attributes. // Looks like below (excluding square brackets): // [ /// text] - private SyntaxTriviaList GetXmlTrivia(string name, SyntaxList attributes, SyntaxList contents, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetXmlTrivia(string name, SyntaxList attributes, XmlTextSyntax contents, SyntaxTriviaList leadingWhitespace) { XmlElementStartTagSyntax start = SyntaxFactory.XmlElementStartTag( SyntaxFactory.Token(SyntaxKind.LessThanToken), @@ -496,8 +662,7 @@ private SyntaxTriviaList GetXmlTrivia(string name, SyntaxList(contents), end); return GetXmlTrivia(element, leadingWhitespace); } diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index a6be51c..526275a 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -35,7 +36,7 @@ private struct SymbolData #pragma warning disable RS1024 // Compare symbols correctly // Bug fixed https://github.com/dotnet/roslyn-analyzers/pull/4571 - private readonly Dictionary ResolvedSymbols = new(); + private readonly Dictionary ResolvedSymbols = new(); #pragma warning restore RS1024 // Compare symbols correctly BinaryLogger? _binLogger = null; @@ -76,7 +77,7 @@ public static void Start(Configuration config) // IMPORTANT: Need to load the MSBuild property before calling the ToTripleSlashPorter constructor. LoadVSInstance(); - ToTripleSlashPorter porter = new ToTripleSlashPorter(config); + var porter = new ToTripleSlashPorter(config); porter.Port(); } @@ -114,8 +115,12 @@ private void Port() Location location = symbol.Locations.FirstOrDefault() ?? throw new NullReferenceException($"No locations found for {docsType.FullName}."); - SyntaxTree tree = location.SourceTree - ?? throw new NullReferenceException($"No tree found in the location of {docsType.FullName}."); + SyntaxTree? tree = location.SourceTree; + if (tree == null) + { + Log.Warning($"No tree found in the location of {docsType.FullName}. Skipping."); + continue; + } if (mainProjectData.Compilation.SyntaxTrees.FirstOrDefault(x => x.FilePath == tree.FilePath) is null) { @@ -135,8 +140,9 @@ private void Port() // Can't reuse the existing Workspace or exception thrown saying we already have the project loaded in this workspace. // Unfortunately, there is no way to retrieve a references project as a Project instance from the existing workspace. - ProjectData extraProjectData = GetProjectData(projectPath); - ResolvedSymbols.Add(symbol, new SymbolData { Api = docsType, ProjectData = extraProjectData }); + ProjectData extraProjectData = GetProjectDataAndSymbol(projectPath, docsType.FullName, out INamedTypeSymbol? actualSymbol); + + ResolvedSymbols.Add(actualSymbol, new SymbolData { Api = docsType, ProjectData = extraProjectData }); } } else @@ -164,7 +170,7 @@ private void Port() } } - private void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) + private static void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) { ImmutableList diagnostics = workspace.Diagnostics; if (diagnostics.Any()) @@ -192,13 +198,34 @@ private void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) } } + private ProjectData GetProjectDataAndSymbol( + string csprojPath, + string symbolFullName, + [NotNull] out INamedTypeSymbol? actualSymbol) + { + ProjectData pd = GetProjectData(csprojPath); + + // Try to find the symbol in the current compilation + actualSymbol = + pd.Compilation.GetTypeByMetadataName(symbolFullName) ?? + pd.Compilation.Assembly.GetTypeByMetadataName(symbolFullName); + + if (actualSymbol == null) + { + Log.Error($"Type symbol not found in compilation: {symbolFullName}."); + throw new NullReferenceException(); + } + + return pd; + } + private ProjectData GetProjectData(string csprojPath) { - ProjectData t = new(); + ProjectData pd = new(); try { - t.Workspace = MSBuildWorkspace.Create(); + pd.Workspace = MSBuildWorkspace.Create(); } catch (ReflectionTypeLoadException) { @@ -206,22 +233,21 @@ private ProjectData GetProjectData(string csprojPath) throw; } - CheckDiagnostics(t.Workspace, "MSBuildWorkspace.Create"); + CheckDiagnostics(pd.Workspace, "MSBuildWorkspace.Create"); - t.Project = t.Workspace.OpenProjectAsync(csprojPath, msbuildLogger: BinLogger).Result + pd.Project = pd.Workspace.OpenProjectAsync(csprojPath, msbuildLogger: BinLogger).Result ?? throw new NullReferenceException($"Could not find the project: {csprojPath}"); - CheckDiagnostics(t.Workspace, $"workspace.OpenProjectAsync - {csprojPath}"); + CheckDiagnostics(pd.Workspace, $"workspace.OpenProjectAsync - {csprojPath}"); - t.Compilation = t.Project.GetCompilationAsync().Result + pd.Compilation = pd.Project.GetCompilationAsync().Result ?? throw new NullReferenceException("The project's compilation was null."); - CheckDiagnostics(t.Workspace, $"project.GetCompilationAsync - {csprojPath}"); + CheckDiagnostics(pd.Workspace, $"project.GetCompilationAsync - {csprojPath}"); - return t; + return pd; } - #region MSBuild loading logic private static readonly Dictionary s_pathsToAssemblies = new(StringComparer.OrdinalIgnoreCase); diff --git a/Program/DocsPortingTool.csproj b/Program/DocsPortingTool.csproj index 6efdea0..47e19b5 100644 --- a/Program/DocsPortingTool.csproj +++ b/Program/DocsPortingTool.csproj @@ -10,6 +10,7 @@ true true 3.0.0 + true diff --git a/Program/Properties/launchSettings.json b/Program/Properties/launchSettings.json index 35956d8..80b814e 100644 --- a/Program/Properties/launchSettings.json +++ b/Program/Properties/launchSettings.json @@ -2,12 +2,14 @@ "profiles": { "Program": { "commandName": "Project", - "commandLineArgs": "-CsProj D:\\runtime\\src\\libraries\\System.IO.Compression\\src\\System.IO.Compression.csproj -Docs D:\\dotnet-api-docs\\xml -Save true -Direction ToTripleSlash -IncludedAssemblies System.IO.Compression,System.IO.Compression.Brotli -SkipInterfaceImplementations true", + "commandLineArgs": "-CsProj D:\\runtime\\src\\libraries\\System.IO.Compression.Brotli\\src\\System.IO.Compression.Brotli.csproj -Docs D:\\dotnet-api-docs\\xml -Save true -Direction ToTripleSlash -IncludedAssemblies System.IO.Compression.Brotli -IncludedNamespaces System.IO.Compression", "environmentVariables": { + "S.Numerics.Vectors": "-CsProj D:\\runtime\\src\\libraries\\System.Numerics.Vectors\\src\\System.Numerics.Vectors.csproj -Docs D:\\dotnet-api-docs\\xml -Save true -Direction ToTripleSlash -IncludedAssemblies System.Numerics.Vectors -IncludedNamespaces System.Numerics,System.Numerics.Vectors", "DOCS_IOT": "D:\\iot\\artifacts\\bin", "DOCS_CORECLR": "D:\\runtime\\artifacts\\bin\\coreclr\\Windows_NT.x64.Release\\IL\\", "DOCS_WINFORMS": "D:\\winforms\\artifacts\\bin\\", "DOCS_WPF": "D:\\wpf\\.tools\\native\\bin\\dotnet-api-docs_netcoreapp3.0\\0.0.0.1\\_intellisense\\\\netcore-3.0\\", + "S.IO.C.Brotli": "-CsProj D:\\runtime\\src\\libraries\\System.IO.Compression.Brotli\\src\\System.IO.Compression.Brotli.csproj -Docs D:\\dotnet-api-docs\\xml -Save true -Direction ToTripleSlash -IncludedAssemblies System.IO.Compression.Brotli -IncludedNamespaces System.IO.Compression", "DOCS_RUNTIME": "D:\\runtime\\artifacts\\bin\\" } } diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs index 884af43..4170247 100644 --- a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs +++ b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs @@ -52,8 +52,7 @@ private static void PortToTripleSlash( c.DirsDocsXml.Add(testData.DocsDir); - var porter = new ToTripleSlashPorter(c, ToTripleSlashPorter.LoadVSInstance()); - porter.Start(); + ToTripleSlashPorter.Start(c); Verify(testData); } diff --git a/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml b/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml index 42af915..2f13585 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml +++ b/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml @@ -1,18 +1,12 @@ - - + + MyAssembly - - - - - - System.Void - This is the sender parameter. This is the e parameter. + This is the MyDelegate typeparam T. This is the MyDelegate summary. To be added. diff --git a/Tests/PortToTripleSlash/TestData/Basic/MyType.xml b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml index c40b945..61abfeb 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/MyType.xml +++ b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml @@ -32,13 +32,9 @@ Multiple lines. Property - MyAssembly - - System.Int32 - This is the MyProperty summary. This is the MyProperty value. @@ -49,7 +45,7 @@ Multiple lines. These are the MyProperty remarks. -Multiple lines. +Multiple lines and a reference to the field . ]]> @@ -61,9 +57,6 @@ Multiple lines. MyAssembly - - System.Int32 - 1 This is the MyField summary. @@ -83,13 +76,9 @@ Multiple lines. Method - MyAssembly - - System.Int32 - This is the MyIntMethod param1 summary. This is the MyIntMethod param2 summary. @@ -104,7 +93,7 @@ These are the MyIntMethod remarks. Multiple lines. -Mentions the `param1` and the . +Mentions the `param1`, the and the `param2`. ]]> @@ -115,14 +104,9 @@ Mentions the `param1` and the . Method - MyAssembly - 4.0.0.0 - - System.Void - This is the MyVoidMethod summary. @@ -139,23 +123,47 @@ Mentions the . ]]> This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . - This is the IndexOutOfRangeException thrown by MyVoidMethod. + This is the IndexOutOfRangeException thrown by MyVoidMethod. + +-or- + +This is the second case. + +Empty newlines should be respected. + + + + + Method + + MyAssembly + + + To be added. + To be added. - + Method - MyAssembly - - System.Void - This is the MyTypeParamMethod typeparam T. + This is the MyTypeParamMethod parameter param1. This is the MyTypeParamMethod summary. - To be added. + + + @@ -164,11 +172,23 @@ Mentions the . MyAssembly - - MyNamespace.MyDelegate - This is the MyEvent summary. + To be added. + + + + + Method + + MyAssembly + + + The first type to add. + The second type to add. + Adds two MyType instances. + The added types. + To be added. diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index d8348c5..b724ff5 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -3,86 +3,112 @@ namespace MyNamespace { /// This is the MyType class summary. - /// + /// These are the MyType class remarks. + /// Multiple lines. public class MyType { /// This is the MyType constructor summary. public MyType() { - } + } /* Trailing comments should remain untouched */ + // Original double slash comments. They should not be replaced (internal). internal MyType(int myProperty) { _myProperty = myProperty; - } + } // Trailing comments should remain untouched + + /// + /// Triple slash comments above private members should remain untouched. + /// + private int _otherProperty; + // Double slash comments above private members should remain untouched. private int _myProperty; /// This is the MyProperty summary. /// This is the MyProperty value. - /// + /// These are the MyProperty remarks. + /// Multiple lines and a reference to the field . public int MyProperty { - get { return _myProperty; } - set { _myProperty = value; } + get { return _myProperty; /* Internal comments should remain untouched. */ } + set { _myProperty = value; } // Internal comments should remain untouched } /// This is the MyField summary. - /// + /// These are the MyField remarks. + /// Multiple lines. public int MyField = 1; /// 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 . - /// These are the MyIntMethod remarks. /// Multiple lines. - /// Mentions the `param1` and the . - /// ]]> + /// Mentions the , the and the . /// This is the ArgumentNullException thrown by MyIntMethod. It mentions the . /// This is the IndexOutOfRangeException thrown by MyIntMethod. public int MyIntMethod(int param1, int param2) { + // Internal comments should remain untouched. return MyField + param1 + param2; } /// This is the MyVoidMethod summary. - /// These are the MyVoidMethod remarks. /// Multiple lines. - /// Mentions the . - /// ]]> + /// Mentions the . /// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . - /// This is the IndexOutOfRangeException thrown by MyVoidMethod. + /// This is the IndexOutOfRangeException thrown by MyVoidMethod. + /// -or- + /// This is the second case. + /// Empty newlines should be respected. public void MyVoidMethod() { } + /// + /// This method simulates a newly added API that did not have documentation in the docs xml. + /// The developer added the documentation in triple slash comments, so they should be preserved + /// and considered the source of truth. + /// + /// + /// These remarks are the source of truth. + /// + public void UndocumentedMethod() + { + } + /// This is the MyTypeParamMethod summary. + /// This is the MyTypeParamMethod parameter param1. /// This is the MyTypeParamMethod typeparam T. - public void MyTypeParamMethod() + /// This is a reference to the typeparam . + /// This is a reference to the parameter . + public void MyTypeParamMethod(int param1) { } /// This is the MyDelegate summary. /// This is the sender parameter. /// This is the e parameter. + /// This is the MyDelegate typeparam T. /// /// /// The .NET Runtime repo. - public delegate void MyDelegate(object sender, object e); + public delegate void MyDelegate(object sender, T e); /// This is the MyEvent summary. public event MyDelegate MyEvent; + + /// Adds two MyType instances. + /// The first type to add. + /// The second type to add. + /// The added types. + public static MyType operator +(MyType value1, MyType value2) + { + return value1; + } } -} +} \ No newline at end of file diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs index 2993f5e..cb1a1c9 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs @@ -4,27 +4,42 @@ namespace MyNamespace { public class MyType { + /// + /// Original triple slash comments. They should be replaced. + /// public MyType() { - } + } /* Trailing comments should remain untouched */ + // Original double slash comments. They should not be replaced (internal). internal MyType(int myProperty) { _myProperty = myProperty; - } + } // Trailing comments should remain untouched + + /// + /// Triple slash comments above private members should remain untouched. + /// + private int _otherProperty; + // Double slash comments above private members should remain untouched. private int _myProperty; + /// + /// Original triple slash comments. They should be replaced. + /// + // Original double slash comments. They should be replaced. public int MyProperty { - get { return _myProperty; } - set { _myProperty = value; } + get { return _myProperty; /* Internal comments should remain untouched. */ } + set { _myProperty = value; } // Internal comments should remain untouched } public int MyField = 1; public int MyIntMethod(int param1, int param2) { + // Internal comments should remain untouched. return MyField + param1 + param2; } @@ -32,12 +47,29 @@ public void MyVoidMethod() { } - public void MyTypeParamMethod() + /// + /// This method simulates a newly added API that did not have documentation in the docs xml. + /// The developer added the documentation in triple slash comments, so they should be preserved + /// and considered the source of truth. + /// + /// + /// These remarks are the source of truth. + /// + public void UndocumentedMethod() { } - public delegate void MyDelegate(object sender, object e); + public void MyTypeParamMethod(int param1) + { + } + + public delegate void MyDelegate(object sender, T e); public event MyDelegate MyEvent; + + public static MyType operator +(MyType value1, MyType value2) + { + return value1; + } } } From 0fdf52233a1c587f1f7c625cf0cce45feaed0bfb Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 15 Jan 2021 12:16:29 -0800 Subject: [PATCH 25/65] Ignore extra info in xrefs when converting to crefs. Convert langwords. --- .../TripleSlashSyntaxRewriter.cs | 50 +++++++++---------- .../TestData/Basic/MyType.xml | 2 + .../TestData/Basic/SourceExpected.cs | 3 +- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index 86d59c2..26cacbc 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -88,6 +88,7 @@ public ... */ internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter { + private static readonly string[] ReservedKeywords = new[] { "abstract", "async", "await", "false", "null", "sealed", "static", "true", "virtual" }; private DocsCommentsContainer DocsComments { get; } private SemanticModel Model { get; } @@ -377,8 +378,8 @@ private static SyntaxTriviaList GetRemarks(DocsAPI api, SyntaxTriviaList leading { if (!api.Remarks.IsDocsEmpty()) { - string text = GetRemarksWithXmlParameters(api); - XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace, markdown: true); + string text = GetRemarksWithXmlElements(api); + XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace); XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(contents); return GetXmlTrivia(xmlRemarks, leadingWhitespace); } @@ -386,20 +387,33 @@ private static SyntaxTriviaList GetRemarks(DocsAPI api, SyntaxTriviaList leading return new(); } - private static string GetRemarksWithXmlParameters(IDocsAPI api) + /// + /// + /// + /// + private static string GetRemarksWithXmlElements(IDocsAPI api) { string remarks = api.Remarks; - if (!api.Remarks.IsDocsEmpty() && ( - api.Params.Any() || api.TypeParams.Any())) + if (!api.Remarks.IsDocsEmpty()) { - MatchCollection collection = Regex.Matches(api.Remarks, @"(?`(?[a-zA-Z0-9_]+)`)"); + remarks = Regex.Replace(remarks, @"", ""); + remarks = Regex.Replace(remarks, @"##[ ]?Remarks(\r?\n)*[\t ]*", ""); + remarks = Regex.Replace(remarks, @"(?[a-zA-Z0-9_\.]+)(?\?[a-zA-Z0-9_]+=[a-zA-Z0-9_])?>)", ""); + + MatchCollection collection = Regex.Matches(api.Remarks, @"(?`(?[a-zA-Z0-9_]+)`)"); foreach (Match match in collection) { string backtickedParam = match.Groups["backtickedParam"].Value; string paramName = match.Groups["paramName"].Value; - if (api.Params.Any(x => x.Name == paramName)) + if(ReservedKeywords.Any(x => x == paramName)) + { + remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); + } + else if (api.Params.Any(x => x.Name == paramName)) { remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); } @@ -409,7 +423,6 @@ private static string GetRemarksWithXmlParameters(IDocsAPI api) } } } - return remarks; } @@ -581,32 +594,15 @@ private static SyntaxTriviaList GetRelateds(List docsRelateds, Synt return relateds; } - private static string ReplaceText(string text, bool markdown) - { - if (markdown) - { - text = Regex.Replace(text, @"", ""); - text = Regex.Replace(text, @"##[ ]?Remarks(\r?\n)*[\t ]*", ""); - text = Regex.Replace(text, @"(?[a-zA-Z0-9_\.]+)>)", ""); - } - else - { - text = text.WithoutDocIdPrefixes(); - } - - return text; - } - /* XmlText XmlTextLiteralNewLineToken (XmlTextSyntax) -> endline XmlTextLiteralToken (XmlTextLiteralToken) -> [ text] Lead: DocumentationCommentExteriorTrivia (SyntaxTrivia) -> [ /// ] */ - private static XmlTextSyntax GetTextAsCommentedTokens(string text, SyntaxTriviaList leadingWhitespace, bool markdown = false) + private static XmlTextSyntax GetTextAsCommentedTokens(string text, SyntaxTriviaList leadingWhitespace) { - text = ReplaceText(text, markdown); + text = text.WithoutDocIdPrefixes(); // collapse newlines to a single one string whitespace = Regex.Replace(leadingWhitespace.ToFullString(), @"(\r?\n)+", ""); diff --git a/Tests/PortToTripleSlash/TestData/Basic/MyType.xml b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml index 61abfeb..54282e2 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/MyType.xml +++ b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml @@ -95,6 +95,8 @@ Multiple lines. Mentions the `param1`, the and the `param2`. +There are also a `true` and a `null`. + ]]> This is the ArgumentNullException thrown by MyIntMethod. It mentions the . diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index b724ff5..8cb3d0c 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -47,7 +47,8 @@ public int MyProperty /// This is the MyIntMethod return value. It mentions the . /// These are the MyIntMethod remarks. /// Multiple lines. - /// Mentions the , the and the . + /// Mentions the , the and the . + /// There are also a and a . /// This is the ArgumentNullException thrown by MyIntMethod. It mentions the . /// This is the IndexOutOfRangeException thrown by MyIntMethod. public int MyIntMethod(int param1, int param2) From 40e585f8068ce68773b625d3742243ef6678d908 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 15 Jan 2021 12:20:44 -0800 Subject: [PATCH 26/65] Added missing test for displayProperty which uncovered an unhandled bug when detecting that string. --- Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs | 2 +- Tests/PortToTripleSlash/TestData/Basic/MyType.xml | 2 +- Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index 26cacbc..96df932 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -401,7 +401,7 @@ private static string GetRemarksWithXmlElements(IDocsAPI api) remarks = Regex.Replace(remarks, @"", ""); remarks = Regex.Replace(remarks, @"##[ ]?Remarks(\r?\n)*[\t ]*", ""); - remarks = Regex.Replace(remarks, @"(?[a-zA-Z0-9_\.]+)(?\?[a-zA-Z0-9_]+=[a-zA-Z0-9_])?>)", ""); + remarks = Regex.Replace(remarks, @"(?[a-zA-Z0-9_\.]+)(?\?[a-zA-Z0-9_]+=[a-zA-Z0-9_]+)?>)", ""); MatchCollection collection = Regex.Matches(api.Remarks, @"(?`(?[a-zA-Z0-9_]+)`)"); diff --git a/Tests/PortToTripleSlash/TestData/Basic/MyType.xml b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml index 54282e2..77a6832 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/MyType.xml +++ b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml @@ -45,7 +45,7 @@ Multiple lines. These are the MyProperty remarks. -Multiple lines and a reference to the field . +Multiple lines and a reference to the field and the xref uses displayProperty, which should be ignored when porting. ]]> diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index 8cb3d0c..fbcb2e9 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -29,7 +29,7 @@ internal MyType(int myProperty) /// This is the MyProperty summary. /// This is the MyProperty value. /// These are the MyProperty remarks. - /// Multiple lines and a reference to the field . + /// Multiple lines and a reference to the field and the xref uses displayProperty, which should be ignored when porting. public int MyProperty { get { return _myProperty; /* Internal comments should remain untouched. */ } From aad9cd8b3a7ec07ca0f72795727403c69a5b4ff8 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 28 Jan 2021 11:36:04 -0800 Subject: [PATCH 27/65] Advanced DocId detection in remarks, excluding prefixes that can't be converted to see crefs. Detect primitive types in see crefs and convert them to their simplified representation. Add unit tests to verify this. --- Libraries/Extensions.cs | 13 -- .../TripleSlashSyntaxRewriter.cs | 173 +++++++++++------- .../TestData/Basic/MyType.xml | 13 +- .../TestData/Basic/SourceExpected.cs | 8 +- 4 files changed, 125 insertions(+), 82 deletions(-) diff --git a/Libraries/Extensions.cs b/Libraries/Extensions.cs index 40a5d63..03966e4 100644 --- a/Libraries/Extensions.cs +++ b/Libraries/Extensions.cs @@ -38,19 +38,6 @@ public static string RemoveSubstrings(this string oldString, params string[] str // Checks if the passed string is considered "empty" according to the Docs repo rules. public static bool IsDocsEmpty(this string? s) => string.IsNullOrWhiteSpace(s) || s == Configuration.ToBeAdded; - - public static string WithoutDocIdPrefixes(this string text) - { - if (text.Length > 2 && text[1] == ':') - { - return text[2..]; - } - - return Regex.Replace( - input: text, - pattern: @"cref=""[a-zA-Z]{1}\:(?[a-zA-Z0-9\._]+)""", - replacement: "cref=\"${cref}\""); - } } } diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index 96df932..2167a0c 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -89,6 +89,27 @@ public ... internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter { private static readonly string[] ReservedKeywords = new[] { "abstract", "async", "await", "false", "null", "sealed", "static", "true", "virtual" }; + + 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" } + }; + private DocsCommentsContainer DocsComments { get; } private SemanticModel Model { get; } @@ -213,6 +234,8 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod #endregion + #region Visit helpers + private SyntaxNode? VisitType(SyntaxNode? node, ISymbol? symbol) { if (node == null || symbol == null) @@ -317,6 +340,38 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod return node; } + private bool TryGetMember(SyntaxNode node, [NotNullWhen(returnValue: true)] out DocsMember? member) + { + member = null; + if (Model.GetDeclaredSymbol(node) is ISymbol symbol) + { + string? docId = symbol.GetDocumentationCommentId(); + if (!string.IsNullOrWhiteSpace(docId)) + { + member = DocsComments.Members.FirstOrDefault(m => m.DocId == docId); + } + } + + return member != null; + } + + private bool TryGetType(ISymbol symbol, [NotNullWhen(returnValue: true)] out DocsType? type) + { + type = null; + + string? docId = symbol.GetDocumentationCommentId(); + if (!string.IsNullOrWhiteSpace(docId)) + { + type = DocsComments.Types.FirstOrDefault(t => t.DocId == docId); + } + + return type != null; + } + + #endregion + + #region Syntax manipulation + private static SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxNode node, params SyntaxTriviaList[] trivias) { SyntaxTriviaList finalTrivia = new(); @@ -387,45 +442,6 @@ private static SyntaxTriviaList GetRemarks(DocsAPI api, SyntaxTriviaList leading return new(); } - /// - /// - /// - /// - private static string GetRemarksWithXmlElements(IDocsAPI api) - { - string remarks = api.Remarks; - - if (!api.Remarks.IsDocsEmpty()) - { - remarks = Regex.Replace(remarks, @"", ""); - remarks = Regex.Replace(remarks, @"##[ ]?Remarks(\r?\n)*[\t ]*", ""); - remarks = Regex.Replace(remarks, @"(?[a-zA-Z0-9_\.]+)(?\?[a-zA-Z0-9_]+=[a-zA-Z0-9_]+)?>)", ""); - - MatchCollection collection = Regex.Matches(api.Remarks, @"(?`(?[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)) - { - remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); - } - else if (api.Params.Any(x => x.Name == paramName)) - { - remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); - } - else if (api.TypeParams.Any(x => x.Name == paramName)) - { - remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); - } - } - } - return remarks; - } - private static SyntaxTriviaList GetValue(DocsMember api, SyntaxTriviaList leadingWhitespace) { if (!api.Value.IsDocsEmpty()) @@ -503,8 +519,7 @@ private static SyntaxTriviaList GetException(string cref, string text, SyntaxTri { if (!text.IsDocsEmpty()) { - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutDocIdPrefixes())); - //XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text.WithoutPrefix(), leadingWhitespace)); + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(RemoveDocIdPrefixes(cref))); XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace); XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); return GetXmlTrivia(element, leadingWhitespace); @@ -529,7 +544,8 @@ private static SyntaxTriviaList GetExceptions(List docsExceptions private static SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace) { - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutDocIdPrefixes())); + cref = ReplacePrimitiveTypes(cref); + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); XmlEmptyElementSyntax element = SyntaxFactory.XmlSeeAlsoElement(crefSyntax); return GetXmlTrivia(element, leadingWhitespace); } @@ -550,7 +566,8 @@ private static SyntaxTriviaList GetSeeAlsos(List docsSeeAlsoCrefs, Synta private static SyntaxTriviaList GetAltMember(string cref, SyntaxTriviaList leadingWhitespace) { - XmlAttributeSyntax attribute = SyntaxFactory.XmlTextAttribute("cref", cref.WithoutDocIdPrefixes()); + cref = ReplacePrimitiveTypes(cref); + XmlAttributeSyntax attribute = SyntaxFactory.XmlTextAttribute("cref", cref); XmlEmptyElementSyntax emptyElement = SyntaxFactory.XmlEmptyElement(SyntaxFactory.XmlName(SyntaxFactory.Identifier("altmember")), new SyntaxList(attribute)); return GetXmlTrivia(emptyElement, leadingWhitespace); } @@ -594,15 +611,9 @@ private static SyntaxTriviaList GetRelateds(List docsRelateds, Synt return relateds; } - /* - XmlText - XmlTextLiteralNewLineToken (XmlTextSyntax) -> endline - XmlTextLiteralToken (XmlTextLiteralToken) -> [ text] - Lead: DocumentationCommentExteriorTrivia (SyntaxTrivia) -> [ /// ] - */ private static XmlTextSyntax GetTextAsCommentedTokens(string text, SyntaxTriviaList leadingWhitespace) { - text = text.WithoutDocIdPrefixes(); + text = ReplacePrimitiveTypes(text); // collapse newlines to a single one string whitespace = Regex.Replace(leadingWhitespace.ToFullString(), @"(\r?\n)+", ""); @@ -662,32 +673,64 @@ private static SyntaxTriviaList GetXmlTrivia(string name, SyntaxList", ""); + remarks = Regex.Replace(remarks, @"##[ ]?Remarks(\r?\n)*[\t ]*", ""); + remarks = Regex.Replace(remarks, @"(?[a-zA-Z0-9_,\.\[\]\(\)`\{\}\@\+\*\&\^\#]+)(?%2A)?(?\?[a-zA-Z0-9_]+=[a-zA-Z0-9_]+)?>)", ""); + + MatchCollection collection = Regex.Matches(api.Remarks, @"(?`(?[a-zA-Z0-9_]+)`)"); + + foreach (Match match in collection) { - member = DocsComments.Members.FirstOrDefault(m => m.DocId == docId); + string backtickedParam = match.Groups["backtickedParam"].Value; + string paramName = match.Groups["paramName"].Value; + if (ReservedKeywords.Any(x => x == paramName)) + { + remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); + } + else if (api.Params.Any(x => x.Name == paramName)) + { + remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); + } + else if (api.TypeParams.Any(x => x.Name == paramName)) + { + remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); + } } - } - return member != null; + remarks = ReplacePrimitiveTypes(remarks); + } + return remarks; } - private bool TryGetType(ISymbol symbol, [NotNullWhen(returnValue: true)] out DocsType? type) + private static string RemoveDocIdPrefixes(string text) { - type = null; - - string? docId = symbol.GetDocumentationCommentId(); - if (!string.IsNullOrWhiteSpace(docId)) + if (text.Length > 2 && text[1] == ':') { - type = DocsComments.Types.FirstOrDefault(t => t.DocId == docId); + return text[2..]; } - return type != null; + text = Regex.Replace(text, @"cref=""[a-zA-Z]{1}\:", "cref=\""); + + return text; } + + private static string ReplacePrimitiveTypes(string text) + { + text = RemoveDocIdPrefixes(text); + foreach ((string key, string value) in PrimitiveTypes) + { + text = Regex.Replace(text, @$" 1 - This is the MyField summary. + This is the MyField summary. + +There is a primitive type here. here. + Multiple lines. ]]> @@ -112,7 +116,8 @@ There are also a `true` and a `null`. This is the MyVoidMethod summary. - + . +Also mentions an overloaded method DocID: . + +And also mentions an overloaded method DocID with displayProperty which should be ignored when porting: . + ]]> This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index fbcb2e9..cabbcd2 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -36,8 +36,10 @@ public int MyProperty set { _myProperty = value; } // Internal comments should remain untouched } - /// This is the MyField summary. + /// This is the MyField summary. + /// There is a primitive type here. /// These are the MyField remarks. + /// There is a primitive type here. /// Multiple lines. public int MyField = 1; @@ -60,7 +62,9 @@ public int MyIntMethod(int param1, int param2) /// This is the MyVoidMethod summary. /// These are the MyVoidMethod remarks. /// Multiple lines. - /// Mentions the . + /// Mentions the . + /// Also mentions an overloaded method DocID: . + /// And also mentions an overloaded method DocID with displayProperty which should be ignored when porting: . /// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . /// This is the IndexOutOfRangeException thrown by MyVoidMethod. /// -or- From 293db4b2a09a04917065fc88fa4ccbdea53fde37 Mon Sep 17 00:00:00 2001 From: Carlos Sanchez <1175054+carlossanlop@users.noreply.github.com> Date: Thu, 28 Jan 2021 11:37:20 -0800 Subject: [PATCH 28/65] Add .NET build and test yml action --- .github/workflows/dotnet.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/dotnet.yml diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..a73cc8b --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,25 @@ +name: .NET + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal From b5e3fae8c30efaf65920cda032800430c740f85f Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:41:29 -0800 Subject: [PATCH 29/65] Move locations --- DocsPortingTool/Analyzer.cs | 898 ------------------ DocsPortingTool/Configuration.cs | 575 ----------- DocsPortingTool/Docs/APIKind.cs | 12 - DocsPortingTool/Docs/DocsAPI.cs | 234 ----- DocsPortingTool/Docs/DocsAssemblyInfo.cs | 38 - DocsPortingTool/Docs/DocsAttribute.cs | 29 - DocsPortingTool/Docs/DocsCommentsContainer.cs | 303 ------ DocsPortingTool/Docs/DocsException.cs | 103 -- DocsPortingTool/Docs/DocsMember.cs | 224 ----- DocsPortingTool/Docs/DocsMemberSignature.cs | 30 - DocsPortingTool/Docs/DocsParam.cs | 41 - DocsPortingTool/Docs/DocsParameter.cs | 27 - DocsPortingTool/Docs/DocsType.cs | 184 ---- DocsPortingTool/Docs/DocsTypeParam.cs | 42 - DocsPortingTool/Docs/DocsTypeParameter.cs | 64 -- DocsPortingTool/Docs/DocsTypeSignature.cs | 30 - DocsPortingTool/Docs/IDocsAPI.cs | 22 - DocsPortingTool/DocsPortingTool.cs | 12 - DocsPortingTool/DocsPortingTool.csproj | 18 - DocsPortingTool/Extensions.cs | 37 - DocsPortingTool/Log.cs | 377 -------- .../Properties/launchSettings.json | 15 - .../TripleSlashCommentsContainer.cs | 186 ---- .../TripleSlash/TripleSlashException.cs | 49 - .../TripleSlash/TripleSlashMember.cs | 160 ---- .../TripleSlash/TripleSlashParam.cs | 44 - .../TripleSlash/TripleSlashTypeParam.cs | 40 - DocsPortingTool/XmlHelper.cs | 319 ------- 28 files changed, 4113 deletions(-) delete mode 100644 DocsPortingTool/Analyzer.cs delete mode 100644 DocsPortingTool/Configuration.cs delete mode 100644 DocsPortingTool/Docs/APIKind.cs delete mode 100644 DocsPortingTool/Docs/DocsAPI.cs delete mode 100644 DocsPortingTool/Docs/DocsAssemblyInfo.cs delete mode 100644 DocsPortingTool/Docs/DocsAttribute.cs delete mode 100644 DocsPortingTool/Docs/DocsCommentsContainer.cs delete mode 100644 DocsPortingTool/Docs/DocsException.cs delete mode 100644 DocsPortingTool/Docs/DocsMember.cs delete mode 100644 DocsPortingTool/Docs/DocsMemberSignature.cs delete mode 100644 DocsPortingTool/Docs/DocsParam.cs delete mode 100644 DocsPortingTool/Docs/DocsParameter.cs delete mode 100644 DocsPortingTool/Docs/DocsType.cs delete mode 100644 DocsPortingTool/Docs/DocsTypeParam.cs delete mode 100644 DocsPortingTool/Docs/DocsTypeParameter.cs delete mode 100644 DocsPortingTool/Docs/DocsTypeSignature.cs delete mode 100644 DocsPortingTool/Docs/IDocsAPI.cs delete mode 100644 DocsPortingTool/DocsPortingTool.cs delete mode 100644 DocsPortingTool/DocsPortingTool.csproj delete mode 100644 DocsPortingTool/Extensions.cs delete mode 100644 DocsPortingTool/Log.cs delete mode 100644 DocsPortingTool/Properties/launchSettings.json delete mode 100644 DocsPortingTool/TripleSlash/TripleSlashCommentsContainer.cs delete mode 100644 DocsPortingTool/TripleSlash/TripleSlashException.cs delete mode 100644 DocsPortingTool/TripleSlash/TripleSlashMember.cs delete mode 100644 DocsPortingTool/TripleSlash/TripleSlashParam.cs delete mode 100644 DocsPortingTool/TripleSlash/TripleSlashTypeParam.cs delete mode 100644 DocsPortingTool/XmlHelper.cs diff --git a/DocsPortingTool/Analyzer.cs b/DocsPortingTool/Analyzer.cs deleted file mode 100644 index 540449a..0000000 --- a/DocsPortingTool/Analyzer.cs +++ /dev/null @@ -1,898 +0,0 @@ -#nullable enable -using DocsPortingTool.Docs; -using DocsPortingTool.TripleSlash; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace DocsPortingTool -{ - public class Analyzer - { - private readonly List ModifiedFiles = new List(); - private readonly List ModifiedTypes = new List(); - private readonly List ModifiedAPIs = new List(); - private readonly List ProblematicAPIs = new List(); - private readonly List AddedExceptions = new List(); - - private int TotalModifiedIndividualElements = 0; - - private readonly TripleSlashCommentsContainer TripleSlashComments; - private readonly DocsCommentsContainer DocsComments; - - private Configuration Config { get; set; } - - public Analyzer(Configuration config) - { - Config = config; - TripleSlashComments = new TripleSlashCommentsContainer(config); - DocsComments = new DocsCommentsContainer(config); - } - - // Do all the magic. - public void Start() - { - TripleSlashComments.CollectFiles(); - - if (TripleSlashComments.TotalFiles > 0) - { - DocsComments.CollectFiles(); - PortMissingComments(); - } - else - { - Log.Error("No triple slash comments found."); - } - - PrintUndocumentedAPIs(); - PrintSummary(); - - DocsComments.Save(); - } - - // Checks if the passed string is considered "empty" according to the Docs repo rules. - internal static bool IsEmpty(string? s) - { - return string.IsNullOrWhiteSpace(s) || s == Configuration.ToBeAdded; - } - - private void PortMissingComments() - { - Log.Info("Looking for triple slash comments that can be ported..."); - - foreach (DocsType dTypeToUpdate in DocsComments.Types) - { - PortMissingCommentsForType(dTypeToUpdate); - } - - foreach (DocsMember dMemberToUpdate in DocsComments.Members) - { - PortMissingCommentsForMember(dMemberToUpdate); - } - } - - // Tries to find a triple slash element from which to port documentation for the specified Docs type. - private void PortMissingCommentsForType(DocsType dTypeToUpdate) - { - TripleSlashMember? tsTypeToPort = TripleSlashComments.Members.FirstOrDefault(x => x.Name == dTypeToUpdate.DocIdEscaped); - if (tsTypeToPort != null) - { - if (tsTypeToPort.Name == dTypeToUpdate.DocIdEscaped) - { - TryPortMissingSummaryForAPI(dTypeToUpdate, tsTypeToPort, null); - TryPortMissingRemarksForAPI(dTypeToUpdate, tsTypeToPort, null, skipInterfaceRemarks: true); - TryPortMissingParamsForAPI(dTypeToUpdate, tsTypeToPort, null); // Some types, like delegates, have params - TryPortMissingTypeParamsForAPI(dTypeToUpdate, tsTypeToPort, null); // Type names ending with have TypeParams - } - - if (dTypeToUpdate.Changed) - { - ModifiedTypes.AddIfNotExists(dTypeToUpdate.DocId); - ModifiedFiles.AddIfNotExists(dTypeToUpdate.FilePath); - } - } - } - - // Tries to find a triple slash element from which to port documentation for the specified Docs member. - private void PortMissingCommentsForMember(DocsMember dMemberToUpdate) - { - string docId = dMemberToUpdate.DocIdEscaped; - TripleSlashMember? tsMemberToPort = TripleSlashComments.Members.FirstOrDefault(x => x.Name == docId); - TryGetEIIMember(dMemberToUpdate, out DocsMember? interfacedMember); - - if (tsMemberToPort != null || interfacedMember != null) - { - TryPortMissingSummaryForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember); - TryPortMissingRemarksForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember, Config.SkipInterfaceRemarks); - TryPortMissingParamsForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember); - TryPortMissingTypeParamsForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember); - TryPortMissingExceptionsForMember(dMemberToUpdate, tsMemberToPort); - - // Properties sometimes don't have a but have a - if (dMemberToUpdate.MemberType == "Property") - { - TryPortMissingPropertyForMember(dMemberToUpdate, tsMemberToPort, interfacedMember); - } - else if (dMemberToUpdate.MemberType == "Method") - { - TryPortMissingMethodForMember(dMemberToUpdate, tsMemberToPort, interfacedMember); - } - - if (dMemberToUpdate.Changed) - { - ModifiedAPIs.AddIfNotExists(dMemberToUpdate.DocId); - ModifiedFiles.AddIfNotExists(dMemberToUpdate.FilePath); - } - } - } - - // Gets a string indicating if an API is an explicit interface implementation, or empty. - private string GetIsEII(bool isEII) - { - return isEII ? " (EII) " : string.Empty; - } - - // Gets a string indicating if an API was created, otherwise it was modified. - private string GetIsCreated(bool created) - { - return created ? "Created" : "Modified"; - } - - // Attempts to obtain the member of the implemented interface. - private bool TryGetEIIMember(IDocsAPI dApiToUpdate, out DocsMember? interfacedMember) - { - interfacedMember = null; - - if (!Config.SkipInterfaceImplementations && dApiToUpdate is DocsMember member) - { - string interfacedMemberDocId = member.ImplementsInterfaceMember; - if (!string.IsNullOrEmpty(interfacedMemberDocId)) - { - interfacedMember = DocsComments.Members.FirstOrDefault(x => x.DocId == interfacedMemberDocId); - return interfacedMember != null; - } - } - - return false; - } - - // Ports the summary for the specified API if the field is undocumented. - private void TryPortMissingSummaryForAPI(IDocsAPI dApiToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember) - { - if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeSummaries || - dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberSummaries) - { - return; - } - - // Only port if undocumented in MS Docs - if (IsEmpty(dApiToUpdate.Summary)) - { - bool isEII = false; - - string name = string.Empty; - string value = string.Empty; - - // Try to port triple slash comments - if (tsMemberToPort != null && !IsEmpty(tsMemberToPort.Summary)) - { - dApiToUpdate.Summary = tsMemberToPort.Summary; - name = tsMemberToPort.Name; - value = tsMemberToPort.Summary; - } - // or try to find if it implements a documented interface - else if (interfacedMember != null && !IsEmpty(interfacedMember.Summary)) - { - dApiToUpdate.Summary = interfacedMember.Summary; - isEII = true; - name = interfacedMember.MemberName; - value = interfacedMember.Summary; - } - - if (!IsEmpty(value)) - { - // Any member can have an empty summary - string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} summary: {name.Escaped()} = {value.Escaped()}"; - PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); - TotalModifiedIndividualElements++; - } - } - } - - // Ports the remarks for the specified API if the field is undocumented. - private void TryPortMissingRemarksForAPI(IDocsAPI dApiToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember, bool skipInterfaceRemarks) - { - if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeRemarks || - dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberRemarks) - { - return; - } - - if (IsEmpty(dApiToUpdate.Remarks)) - { - bool isEII = false; - string name = string.Empty; - string value = string.Empty; - - // Try to port triple slash comments - if (tsMemberToPort != null && !IsEmpty(tsMemberToPort.Remarks)) - { - dApiToUpdate.Remarks = tsMemberToPort.Remarks; - name = tsMemberToPort.Name; - value = tsMemberToPort.Remarks; - } - // or try to find if it implements a documented interface - // which only happens in docs members (types have a null interfacedMember passed) - else if (interfacedMember != null && !IsEmpty(interfacedMember.Remarks)) - { - DocsMember memberToUpdate = (DocsMember)dApiToUpdate; - - // Only attempt to port if the member name is the same as the interfaced member docid without prefix - if (memberToUpdate.MemberName == interfacedMember.DocId[2..]) - { - string dMemberToUpdateTypeDocIdNoPrefix = memberToUpdate.ParentType.DocId[2..]; - string interfacedMemberTypeDocIdNoPrefix = interfacedMember.ParentType.DocId[2..]; - - // Special text for EIIs in Remarks - string eiiMessage = $"This member is an explicit interface member implementation. It can be used only when the instance is cast to an interface.{Environment.NewLine + Environment.NewLine}"; - - string cleanedInterfaceRemarks = string.Empty; - if (!interfacedMember.Remarks.Contains(Configuration.ToBeAdded)) - { - cleanedInterfaceRemarks = interfacedMember.Remarks.RemoveSubstrings("##Remarks", "## Remarks", ""); - } - - // Only port the interface remarks if the user desired that - if (!skipInterfaceRemarks) - { - dApiToUpdate.Remarks = eiiMessage + cleanedInterfaceRemarks; - } - // Otherwise, always add the EII special message - else - { - dApiToUpdate.Remarks = eiiMessage; - } - - name = interfacedMember.MemberName; - value = dApiToUpdate.Remarks; - - isEII = true; - } - } - - if (!IsEmpty(value)) - { - // Any member can have an empty remark - string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} remarks: {name.Escaped()} = {value.Escaped()}"; - PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); - TotalModifiedIndividualElements++; - } - } - } - - // Ports all the parameter descriptions for the specified API if any of them is undocumented. - private void TryPortMissingParamsForAPI(IDocsAPI dApiToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember) - { - if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeParams || - dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberParams) - { - return; - } - - bool created; - bool isEII; - string name; - string value; - - if (tsMemberToPort != null) - { - foreach (DocsParam dParam in dApiToUpdate.Params) - { - if (IsEmpty(dParam.Value)) - { - created = false; - isEII = false; - name = string.Empty; - value = string.Empty; - - TripleSlashParam? tsParam = tsMemberToPort.Params.FirstOrDefault(x => x.Name == dParam.Name); - - // When not found, it's a bug in Docs (param name not the same as source/ref), so need to ask the user to indicate correct name - if (tsParam == null) - { - ProblematicAPIs.AddIfNotExists($"Param=[{dParam.Name}] in Member DocId=[{dApiToUpdate.DocId}]"); - - if (tsMemberToPort.Params.Count() == 0) - { - ProblematicAPIs.AddIfNotExists($"Param=[{dParam.Name}] in Member DocId=[{dApiToUpdate.DocId}]"); - Log.Warning($" There were no triple slash comments for param '{dParam.Name}' in {dApiToUpdate.DocId}"); - } - else - { - created = TryPromptParam(dParam, tsMemberToPort, out TripleSlashParam? newTsParam); - if (newTsParam == null) - { - Log.Error($" There param '{dParam.Name}' was not found in triple slash for {dApiToUpdate.DocId}"); - } - else - { - // Now attempt to document it - if (!IsEmpty(newTsParam.Value)) - { - // try to port triple slash comments - dParam.Value = newTsParam.Value; - name = newTsParam.Name; - value = newTsParam.Value; - } - // or try to find if it implements a documented interface - else if (interfacedMember != null) - { - DocsParam? interfacedParam = interfacedMember.Params.FirstOrDefault(x => x.Name == newTsParam.Name || x.Name == dParam.Name); - if (interfacedParam != null) - { - dParam.Value = interfacedParam.Value; - name = interfacedParam.Name; - value = interfacedParam.Value; - isEII = true; - } - } - } - } - } - // Attempt to port - else if (!IsEmpty(tsParam.Value)) - { - // try to port triple slash comments - dParam.Value = tsParam.Value; - name = tsParam.Name; - value = tsParam.Value; - } - // or try to find if it implements a documented interface - else if (interfacedMember != null) - { - DocsParam? interfacedParam = interfacedMember.Params.FirstOrDefault(x => x.Name == dParam.Name); - if (interfacedParam != null) - { - dParam.Value = interfacedParam.Value; - name = interfacedParam.Name; - value = interfacedParam.Value; - isEII = true; - } - } - - - if (!IsEmpty(value)) - { - string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} ({GetIsCreated(created)}) param {name.Escaped()} = {value.Escaped()}"; - PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); - TotalModifiedIndividualElements++; - } - } - } - } - else if (interfacedMember != null) - { - foreach (DocsParam dParam in dApiToUpdate.Params) - { - if (IsEmpty(dParam.Value)) - { - DocsParam? interfacedParam = interfacedMember.Params.FirstOrDefault(x => x.Name == dParam.Name); - if (interfacedParam != null && !IsEmpty(interfacedParam.Value)) - { - dParam.Value = interfacedParam.Value; - - string message = $"{dApiToUpdate.Kind} EII ({GetIsCreated(false)}) param {dParam.Name.Escaped()} = {dParam.Value.Escaped()}"; - PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); - TotalModifiedIndividualElements++; - } - } - } - } - } - - // Ports all the type parameter descriptions for the specified API if any of them is undocumented. - private void TryPortMissingTypeParamsForAPI(IDocsAPI dApiToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember) - { - if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeTypeParams || - dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberTypeParams) - { - return; - } - - if (tsMemberToPort != null) - { - foreach (TripleSlashTypeParam tsTypeParam in tsMemberToPort.TypeParams) - { - bool isEII = false; - string name = string.Empty; - string value = string.Empty; - - DocsTypeParam? dTypeParam = dApiToUpdate.TypeParams.FirstOrDefault(x => x.Name == tsTypeParam.Name); - - bool created = false; - if (dTypeParam == null) - { - ProblematicAPIs.AddIfNotExists($"TypeParam=[{tsTypeParam.Name}] in Member=[{dApiToUpdate.DocId}]"); - dTypeParam = dApiToUpdate.AddTypeParam(tsTypeParam.Name, XmlHelper.GetNodesInPlainText(tsTypeParam.XETypeParam)); - created = true; - } - - // But it can still be empty, try to retrieve it - if (IsEmpty(dTypeParam.Value)) - { - // try to port triple slash comments - if (!IsEmpty(tsTypeParam.Value)) - { - name = tsTypeParam.Name; - value = tsTypeParam.Value; - } - // or try to find if it implements a documented interface - else if (interfacedMember != null) - { - DocsTypeParam? interfacedTypeParam = interfacedMember.TypeParams.FirstOrDefault(x => x.Name == dTypeParam.Name); - if (interfacedTypeParam != null) - { - name = interfacedTypeParam.Name; - value = interfacedTypeParam.Value; - isEII = true; - } - } - } - - if (!IsEmpty(value)) - { - dTypeParam.Value = value; - string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} ({GetIsCreated(created)}) typeparam {name.Escaped()} = {value.Escaped()}"; - PrintModifiedMember(message, dTypeParam.ParentAPI.FilePath, dApiToUpdate.DocId); - TotalModifiedIndividualElements++; - } - } - } - } - - // Tries to document the passed property. - private void TryPortMissingPropertyForMember(DocsMember dMemberToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember) - { - if (!Config.PortMemberProperties) - { - return; - } - - if (IsEmpty(dMemberToUpdate.Value)) - { - string name = string.Empty; - string value = string.Empty; - bool isEII = false; - - // Issue: sometimes properties have their TS string in Value, sometimes in Returns - if (tsMemberToPort != null) - { - name = tsMemberToPort.Name; - if (!IsEmpty(tsMemberToPort.Value)) - { - value = tsMemberToPort.Value; - } - else if (!IsEmpty(tsMemberToPort.Returns)) - { - value = tsMemberToPort.Returns; - } - } - // or try to find if it implements a documented interface - else if (interfacedMember != null) - { - name = interfacedMember.MemberName; - if (!IsEmpty(interfacedMember.Value)) - { - value = interfacedMember.Value; - } - else if (!IsEmpty(interfacedMember.Returns)) - { - value = interfacedMember.Returns; - } - if (!string.IsNullOrEmpty(value)) - { - isEII = true; - } - } - - if (!IsEmpty(value)) - { - dMemberToUpdate.Value = value; - string message = $"Member {GetIsEII(isEII)} property {name.Escaped()} = {value.Escaped()}"; - PrintModifiedMember(message, dMemberToUpdate.FilePath,dMemberToUpdate.DocId); - TotalModifiedIndividualElements++; - } - } - } - - // Tries to document the passed method. - private void TryPortMissingMethodForMember(DocsMember dMemberToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember) - { - if (!Config.PortMemberReturns) - { - return; - } - - if (IsEmpty(dMemberToUpdate.Returns)) - { - string name = string.Empty; - string value = string.Empty; - bool isEII = false; - - // Bug: Sometimes a void return value shows up as not documented, skip those - if (dMemberToUpdate.ReturnType == "System.Void") - { - ProblematicAPIs.AddIfNotExists($"Unexpected System.Void return value in Method=[{dMemberToUpdate.DocId}]"); - } - else if (tsMemberToPort != null && !IsEmpty(tsMemberToPort.Returns)) - { - name = tsMemberToPort.Name; - value = tsMemberToPort.Returns; - } - else if (interfacedMember != null && !IsEmpty(interfacedMember.Returns)) - { - name = interfacedMember.MemberName; - value = interfacedMember.Returns; - isEII = true; - } - - if (!IsEmpty(value)) - { - dMemberToUpdate.Returns = value; - string message = $"Method {GetIsEII(isEII)} returns {name.Escaped()} = {value.Escaped()}"; - PrintModifiedMember(message, dMemberToUpdate.FilePath, dMemberToUpdate.DocId); - TotalModifiedIndividualElements++; - } - } - } - - // Ports all the exceptions for the specified API. - // They are only processed if the user specified in the command arguments to NOT skip exceptions. - // All exceptions get ported, because there is no easy way to determine if an exception is already documented or not. - private void TryPortMissingExceptionsForMember(DocsMember dMemberToUpdate, TripleSlashMember? tsMemberToPort) - { - if (!Config.PortExceptionsExisting && !Config.PortExceptionsNew) - { - return; - } - - if (tsMemberToPort != null) - { - // Exceptions are a special case: If a new one is found in code, but does not exist in docs, the whole element needs to be added - foreach (TripleSlashException tsException in tsMemberToPort.Exceptions) - { - DocsException? dException = dMemberToUpdate.Exceptions.FirstOrDefault(x => x.Cref == tsException.Cref); - bool created = false; - - // First time adding the cref - if (dException == null && Config.PortExceptionsNew) - { - AddedExceptions.AddIfNotExists($"Exception=[{tsException.Cref}] in Member=[{dMemberToUpdate.DocId}]"); - string text = XmlHelper.ReplaceExceptionPatterns(XmlHelper.GetNodesInPlainText(tsException.XEException)); - dException = dMemberToUpdate.AddException(tsException.Cref, text); - created = true; - } - // If cref exists, check if the text has already been appended - else if (dException != null && Config.PortExceptionsExisting) - { - XElement formattedException = tsException.XEException; - string value = XmlHelper.ReplaceExceptionPatterns(XmlHelper.GetNodesInPlainText(formattedException)); - if (!dException.WordCountCollidesAboveThreshold(value, Config.ExceptionCollisionThreshold)) - { - AddedExceptions.AddIfNotExists($"Exception=[{tsException.Cref}] in Member=[{dMemberToUpdate.DocId}]"); - dException.AppendException(value); - created = true; - } - } - - if (dException != null) - { - if (created || (!IsEmpty(tsException.Value) && IsEmpty(dException.Value))) - { - string message = string.Format($"Exception ({GetIsCreated(created)}) {dException.Cref.Escaped()} = {dException.Value.Escaped()}"); - PrintModifiedMember(message, dException.ParentAPI.FilePath, dException.Cref); - - TotalModifiedIndividualElements++; - } - } - } - } - } - - // If a Param is found in a DocsType or a DocsMember that did not exist in the Triple Slash member, it's possible the param was unexpectedly saved in the triple slash comments with a different name, so the user gets prompted to look for it. - private bool TryPromptParam(DocsParam oldDParam, TripleSlashMember tsMember, out TripleSlashParam? newTsParam) - { - newTsParam = null; - - if (Config.DisablePrompts) - { - Log.Error($"Prompts disabled. Will not process the '{oldDParam.Name}' param."); - return false; - } - - bool created = false; - int option = -1; - while (option == -1) - { - Log.Error($"Problem in param '{oldDParam.Name}' in member '{tsMember.Name}' in file '{oldDParam.ParentAPI.FilePath}'"); - Log.Error($"The param probably exists in code, but the exact name was not found in Docs. What would you like to do?"); - Log.Warning(" 0 - Exit program."); - Log.Info(" 1 - Select the correct triple slash param from the existing ones."); - Log.Info(" 2 - Ignore this param."); - Log.Warning(" Note:Make sure to double check the affected Docs file after the tool finishes executing."); - Log.Cyan(false, "Your answer [0,1,2]: "); - - if (!int.TryParse(Console.ReadLine(), out option)) - { - Log.Error("Not a number. Try again."); - option = -1; - } - else - { - switch (option) - { - case 0: - { - Log.Info("Goodbye!"); - Environment.Exit(0); - break; - } - - case 1: - { - int paramSelection = -1; - while (paramSelection == -1) - { - Log.Info($"Triple slash params found in member '{tsMember.Name}':"); - Log.Warning(" 0 - Exit program."); - int paramCounter = 1; - foreach (TripleSlashParam param in tsMember.Params) - { - Log.Info($" {paramCounter} - {param.Name}"); - paramCounter++; - } - - Log.Cyan(false, $"Your answer to match param '{oldDParam.Name}'? [0..{paramCounter - 1}]: "); - - if (!int.TryParse(Console.ReadLine(), out paramSelection)) - { - Log.Error("Not a number. Try again."); - paramSelection = -1; - } - else if (paramSelection < 0 || paramSelection >= paramCounter) - { - Log.Error("Invalid selection. Try again."); - paramSelection = -1; - } - else if (paramSelection == 0) - { - Log.Info("Goodbye!"); - Environment.Exit(0); - } - else - { - newTsParam = tsMember.Params[paramSelection - 1]; - Log.Success($"Selected: {newTsParam.Name}"); - } - } - - break; - } - - case 2: - { - Log.Info("Skipping this param."); - break; - } - - default: - { - Log.Error("Invalid selection. Try again."); - option = -1; - break; - } - } - } - } - - return created; - } - - /// - /// Standard formatted print message for a modified element. - /// - /// The friendly description of the modified API. - /// The file where the modified API lives. - /// The API name in the triple slash file. - /// The API name in the Docs file. - /// The value that was found in the triple slash file. - /// The value that was found in the Docs file. - private void PrintModifiedMember(string message, string docsFilePath, string docId) - { - Log.Warning($" File: {docsFilePath}"); - Log.Warning($" DocID: {docId}"); - Log.Warning($" {message}"); - Log.Info("---------------------------------------------------"); - Log.Line(); - } - - // Prints all the undocumented APIs. - // This is only done if the user specified in the command arguments to print undocumented APIs. - private void PrintUndocumentedAPIs() - { - if (Config.PrintUndoc) - { - Log.Line(); - Log.Success("-----------------"); - Log.Success("UNDOCUMENTED APIS"); - Log.Success("-----------------"); - - Log.Line(); - - static void TryPrintType(ref bool undocAPI, string typeDocId) - { - if (!undocAPI) - { - Log.Info(" Type: {0}", typeDocId); - undocAPI = true; - } - }; - - static void TryPrintMember(ref bool undocMember, string memberDocId) - { - if (!undocMember) - { - Log.Info(" {0}", memberDocId); - undocMember = true; - } - }; - - int typeSummaries = 0; - int memberSummaries = 0; - int memberValues = 0; - int memberReturns = 0; - int memberParams = 0; - int memberTypeParams = 0; - int exceptions = 0; - - Log.Info("Undocumented APIs:"); - foreach (DocsType docsType in DocsComments.Types) - { - bool undocAPI = false; - if (IsEmpty(docsType.Summary)) - { - TryPrintType(ref undocAPI, docsType.DocId); - Log.Error($" Type Summary: {docsType.Summary}"); - typeSummaries++; - } - } - - foreach (DocsMember member in DocsComments.Members) - { - bool undocMember = false; - - if (IsEmpty(member.Summary)) - { - TryPrintMember(ref undocMember, member.DocId); - - Log.Error($" Member Summary: {member.Summary}"); - memberSummaries++; - } - - if (member.MemberType == "Property") - { - if (member.Value == Configuration.ToBeAdded) - { - TryPrintMember(ref undocMember, member.DocId); - - Log.Error($" Property Value: {member.Value}"); - memberValues++; - } - } - else if (member.MemberType == "Method") - { - if (member.Returns == Configuration.ToBeAdded) - { - TryPrintMember(ref undocMember, member.DocId); - - Log.Error($" Method Returns: {member.Returns}"); - memberReturns++; - } - } - - foreach (DocsParam param in member.Params) - { - if (IsEmpty(param.Value)) - { - TryPrintMember(ref undocMember, member.DocId); - - Log.Error($" Member Param: {param.Name}: {param.Value}"); - memberParams++; - } - } - - foreach (DocsTypeParam typeParam in member.TypeParams) - { - if (IsEmpty(typeParam.Value)) - { - TryPrintMember(ref undocMember, member.DocId); - - Log.Error($" Member Type Param: {typeParam.Name}: {typeParam.Value}"); - memberTypeParams++; - } - } - - foreach (DocsException exception in member.Exceptions) - { - if (IsEmpty(exception.Value)) - { - TryPrintMember(ref undocMember, member.DocId); - - Log.Error($" Member Exception: {exception.Cref}: {exception.Value}"); - exceptions++; - } - } - } - - Log.Info($" Undocumented type summaries: {typeSummaries}"); - Log.Info($" Undocumented member summaries: {memberSummaries}"); - Log.Info($" Undocumented method returns: {memberReturns}"); - Log.Info($" Undocumented property values: {memberValues}"); - Log.Info($" Undocumented member params: {memberParams}"); - Log.Info($" Undocumented member type params: {memberTypeParams}"); - Log.Info($" Undocumented exceptions: {exceptions}"); - - Log.Line(); - } - } - - // Prints a final summary of the execution findings. - private void PrintSummary() - { - Log.Line(); - Log.Success("---------"); - Log.Success("FINISHED!"); - Log.Success("---------"); - - Log.Line(); - Log.Info($"Total modified files: {ModifiedFiles.Count}"); - foreach (string file in ModifiedFiles) - { - Log.Success($" - {file}"); - } - - Log.Line(); - Log.Info($"Total modified types: {ModifiedTypes.Count}"); - foreach (string type in ModifiedTypes) - { - Log.Success($" - {type}"); - } - - Log.Line(); - Log.Info($"Total modified APIs: {ModifiedAPIs.Count}"); - foreach (string api in ModifiedAPIs) - { - Log.Success($" - {api}"); - } - - Log.Line(); - Log.Info($"Total problematic APIs: {ProblematicAPIs.Count}"); - foreach (string api in ProblematicAPIs) - { - Log.Warning($" - {api}"); - } - - Log.Line(); - Log.Info($"Total added exceptions: {AddedExceptions.Count}"); - foreach (string exception in AddedExceptions) - { - Log.Success($" - {exception}"); - } - - Log.Line(); - Log.Info(false, "Total modified individual elements: "); - Log.Success($"{TotalModifiedIndividualElements}"); - } - } -} diff --git a/DocsPortingTool/Configuration.cs b/DocsPortingTool/Configuration.cs deleted file mode 100644 index d48c7d5..0000000 --- a/DocsPortingTool/Configuration.cs +++ /dev/null @@ -1,575 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace DocsPortingTool -{ - public class Configuration - { - private static readonly char Separator = ','; - - private enum Mode - { - DisablePrompts, - Docs, - ExceptionCollisionThreshold, - ExcludedAssemblies, - ExcludedNamespaces, - ExcludedTypes, - IncludedAssemblies, - IncludedNamespaces, - IncludedTypes, - Initial, - PortExceptionsExisting, - PortExceptionsNew, - PortMemberParams, - PortMemberProperties, - PortMemberReturns, - PortMemberRemarks, - PortMemberSummaries, - PortMemberTypeParams, - PortTypeParams, // Params of a Type - PortTypeRemarks, - PortTypeSummaries, - PortTypeTypeParams, // TypeParams of a Type - PrintUndoc, - Save, - SkipInterfaceImplementations, - SkipInterfaceRemarks, - TripleSlash - } - - public static readonly string ToBeAdded = "To be added."; - - public static readonly string[] ForbiddenDirectories = new[] { "binplacePackages", "docs", "mscorlib", "native", "netfx", "netstandard", "pkg", "Product", "ref", "runtime", "shimsTargetRuntime", "testhost", "tests", "winrt" }; - - public List DirsTripleSlashXmls { get; } = new List(); - public List DirsDocsXml { get; } = new List(); - - public HashSet IncludedAssemblies { get; } = new HashSet(); - public HashSet ExcludedAssemblies { get; } = new HashSet(); - public HashSet IncludedNamespaces { get; } = new HashSet(); - public HashSet ExcludedNamespaces { get; } = new HashSet(); - public HashSet IncludedTypes { get; } = new HashSet(); - public HashSet ExcludedTypes { get; } = new HashSet(); - - public bool DisablePrompts { get; set; } = false; - public int ExceptionCollisionThreshold { get; set; } = 70; - public bool PortExceptionsExisting { get; set; } = false; - public bool PortExceptionsNew { get; set; } = true; - public bool PortMemberParams { get; set; } = true; - public bool PortMemberProperties { get; set; } = true; - public bool PortMemberReturns { get; set; } = true; - public bool PortMemberRemarks { get; set; } = true; - public bool PortMemberSummaries { get; set; } = true; - public bool PortMemberTypeParams { get; set; } = true; - /// - /// Params of a Type. - /// - public bool PortTypeParams { get; set; } = true; - public bool PortTypeRemarks { get; set; } = true; - public bool PortTypeSummaries { get; set; } = true; - /// - /// TypeParams of a Type. - /// - public bool PortTypeTypeParams { get; set; } = true; - public bool PrintUndoc { get; set; } = false; - public bool Save { get; set; } = false; - public bool SkipInterfaceImplementations { get; set; } = false; - public bool SkipInterfaceRemarks { get; set; } = true; - - public static Configuration GetFromCommandLineArguments(string[] args) - { - Mode mode = Mode.Initial; - - Log.Info("Verifying CLI arguments..."); - - if (args == null || args.Length == 0) - { - Log.LogErrorPrintHelpAndExit("No arguments passed to the executable."); - } - - Configuration config = new Configuration(); - - foreach (string arg in args!) - { - switch (mode) - { - case Mode.DisablePrompts: - { - config.DisablePrompts = ParseOrExit(arg, "Disable prompts"); - mode = Mode.Initial; - break; - } - - case Mode.Docs: - { - string[] splittedDirPaths = arg.Split(',', StringSplitOptions.RemoveEmptyEntries); - - Log.Cyan($"Specified Docs xml locations:"); - foreach (string dirPath in splittedDirPaths) - { - DirectoryInfo dirInfo = new DirectoryInfo(dirPath); - if (!dirInfo.Exists) - { - Log.LogErrorAndExit($"This Docs xml directory does not exist: {dirPath}"); - } - - config.DirsDocsXml.Add(dirInfo); - Log.Info($" - {dirPath}"); - } - - mode = Mode.Initial; - break; - - } - - case Mode.ExceptionCollisionThreshold: - { - if (!int.TryParse(arg, out int value)) - { - Log.LogErrorAndExit($"Invalid int value for 'Exception collision threshold' argument: {arg}"); - } - else if (value < 1 || value > 100) - { - Log.LogErrorAndExit($"Value needs to be between 0 and 100: {value}"); - } - - config.ExceptionCollisionThreshold = value; - - Log.Cyan($"Exception collision threshold:"); - Log.Info($" - {value}"); - break; - } - - case Mode.ExcludedAssemblies: - { - string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); - - if (splittedArg.Length > 0) - { - Log.Cyan("Excluded assemblies:"); - foreach (string assembly in splittedArg) - { - Log.Cyan($" - {assembly}"); - config.ExcludedAssemblies.Add(assembly); - } - } - else - { - Log.LogErrorPrintHelpAndExit("You must specify at least one assembly."); - } - - mode = Mode.Initial; - break; - } - - case Mode.ExcludedNamespaces: - { - string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); - - if (splittedArg.Length > 0) - { - Log.Cyan("Excluded namespaces:"); - foreach (string ns in splittedArg) - { - Log.Cyan($" - {ns}"); - config.ExcludedNamespaces.Add(ns); - } - } - else - { - Log.LogErrorPrintHelpAndExit("You must specify at least one namespace."); - } - - mode = Mode.Initial; - break; - } - - case Mode.ExcludedTypes: - { - string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); - - if (splittedArg.Length > 0) - { - Log.Cyan($"Excluded types:"); - foreach (string typeName in splittedArg) - { - Log.Cyan($" - {typeName}"); - config.ExcludedTypes.Add(typeName); - } - } - else - { - Log.LogErrorPrintHelpAndExit("You must specify at least one type name."); - } - - mode = Mode.Initial; - break; - } - - case Mode.IncludedAssemblies: - { - string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); - - if (splittedArg.Length > 0) - { - Log.Cyan($"Included assemblies:"); - foreach (string assembly in splittedArg) - { - Log.Cyan($" - {assembly}"); - config.IncludedAssemblies.Add(assembly); - } - } - else - { - Log.LogErrorPrintHelpAndExit("You must specify at least one assembly."); - } - - mode = Mode.Initial; - break; - } - - case Mode.IncludedNamespaces: - { - string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); - - if (splittedArg.Length > 0) - { - Log.Cyan($"Included namespaces:"); - foreach (string ns in splittedArg) - { - Log.Cyan($" - {ns}"); - config.IncludedNamespaces.Add(ns); - } - } - else - { - Log.LogErrorPrintHelpAndExit("You must specify at least one namespace."); - } - - mode = Mode.Initial; - break; - } - - case Mode.IncludedTypes: - { - string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); - - if (splittedArg.Length > 0) - { - Log.Cyan($"Included types:"); - foreach (string typeName in splittedArg) - { - Log.Cyan($" - {typeName}"); - config.IncludedTypes.Add(typeName); - } - } - else - { - Log.LogErrorPrintHelpAndExit("You must specify at least one type name."); - } - - mode = Mode.Initial; - break; - } - - case Mode.Initial: - { - switch (arg.ToUpperInvariant()) - { - case "-DOCS": - mode = Mode.Docs; - break; - - case "-DISABLEPROMPTS": - mode = Mode.DisablePrompts; - break; - - case "EXCEPTIONCOLLISIONTHRESHOLD": - mode = Mode.ExceptionCollisionThreshold; - break; - - case "-EXCLUDEDASSEMBLIES": - mode = Mode.ExcludedAssemblies; - break; - - case "-EXCLUDEDNAMESPACES": - mode = Mode.ExcludedNamespaces; - break; - - case "-EXCLUDEDTYPES": - mode = Mode.ExcludedTypes; - break; - - case "-H": - case "-HELP": - Log.PrintHelp(); - Environment.Exit(0); - break; - - case "-INCLUDEDASSEMBLIES": - mode = Mode.IncludedAssemblies; - break; - - case "-INCLUDEDNAMESPACES": - mode = Mode.IncludedNamespaces; - break; - - case "-INCLUDEDTYPES": - mode = Mode.IncludedTypes; - break; - - case "-PORTEXCEPTIONSEXISTING": - mode = Mode.PortExceptionsExisting; - break; - - case "-PORTEXCEPTIONSNEW": - mode = Mode.PortExceptionsNew; - break; - - case "-PORTMEMBERPARAMS": - mode = Mode.PortMemberParams; - break; - - case "-PORTMEMBERPROPERTIES": - mode = Mode.PortMemberProperties; - break; - - case "-PORTMEMBERRETURNS": - mode = Mode.PortMemberReturns; - break; - - case "-PORTMEMBERREMARKS": - mode = Mode.PortMemberRemarks; - break; - - case "-PORTMEMBERSUMMARIES": - mode = Mode.PortMemberSummaries; - break; - - case "-PORTMEMBERTYPEPARAMS": - mode = Mode.PortMemberTypeParams; - break; - - case "-PORTTYPEPARAMS": // Params of a Type - mode = Mode.PortTypeParams; - break; - - case "-PORTTYPEREMARKS": - mode = Mode.PortTypeRemarks; - break; - - case "-PORTTYPESUMMARIES": - mode = Mode.PortTypeSummaries; - break; - - case "-PORTTYPETYPEPARAMS": // TypeParams of a Type - mode = Mode.PortTypeTypeParams; - break; - - case "-PRINTUNDOC": - mode = Mode.PrintUndoc; - break; - - case "-SAVE": - mode = Mode.Save; - break; - - case "-SKIPINTERFACEIMPLEMENTATIONS": - mode = Mode.SkipInterfaceImplementations; - break; - - case "-SKIPINTERFACEREMARKS": - mode = Mode.SkipInterfaceRemarks; - break; - - case "-TRIPLESLASH": - mode = Mode.TripleSlash; - break; - default: - Log.LogErrorPrintHelpAndExit($"Unrecognized argument: {arg}"); - break; - } - break; - } - - case Mode.PortExceptionsExisting: - { - config.PortExceptionsExisting = ParseOrExit(arg, "Port existing exceptions"); - mode = Mode.Initial; - break; - } - - case Mode.PortExceptionsNew: - { - config.PortExceptionsNew = ParseOrExit(arg, "Port new exceptions"); - mode = Mode.Initial; - break; - } - - case Mode.PortMemberParams: - { - config.PortMemberParams = ParseOrExit(arg, "Port member Params"); - mode = Mode.Initial; - break; - } - - case Mode.PortMemberProperties: - { - config.PortMemberProperties = ParseOrExit(arg, "Port member Properties"); - mode = Mode.Initial; - break; - } - - case Mode.PortMemberRemarks: - { - config.PortMemberRemarks = ParseOrExit(arg, "Port member Remarks"); - mode = Mode.Initial; - break; - } - - case Mode.PortMemberReturns: - { - config.PortMemberReturns = ParseOrExit(arg, "Port member Returns"); - mode = Mode.Initial; - break; - } - - case Mode.PortMemberSummaries: - { - config.PortMemberSummaries = ParseOrExit(arg, "Port member Summaries"); - mode = Mode.Initial; - break; - } - - case Mode.PortMemberTypeParams: - { - config.PortMemberTypeParams = ParseOrExit(arg, "Port member TypeParams"); - mode = Mode.Initial; - break; - } - - case Mode.PortTypeParams: // Params of a Type - { - config.PortTypeParams = ParseOrExit(arg, "Port Type Params"); - mode = Mode.Initial; - break; - } - - case Mode.PortTypeRemarks: - { - config.PortTypeRemarks = ParseOrExit(arg, "Port Type Remarks"); - mode = Mode.Initial; - break; - } - - case Mode.PortTypeSummaries: - { - config.PortTypeSummaries = ParseOrExit(arg, "Port Type Summaries"); - mode = Mode.Initial; - break; - } - - case Mode.PortTypeTypeParams: // TypeParams of a Type - { - config.PortTypeTypeParams = ParseOrExit(arg, "Port Type TypeParams"); - mode = Mode.Initial; - break; - } - - case Mode.PrintUndoc: - { - config.PrintUndoc = ParseOrExit(arg, "Print undoc"); - mode = Mode.Initial; - break; - } - - case Mode.Save: - { - config.Save = ParseOrExit(arg, "Save"); - mode = Mode.Initial; - break; - } - - case Mode.SkipInterfaceImplementations: - { - config.SkipInterfaceImplementations = ParseOrExit(arg, "Skip interface implementations"); - mode = Mode.Initial; - break; - } - - case Mode.SkipInterfaceRemarks: - { - config.SkipInterfaceRemarks = ParseOrExit(arg, "Skip appending interface remarks"); - mode = Mode.Initial; - break; - } - - case Mode.TripleSlash: - { - string[] splittedDirPaths = arg.Split(',', StringSplitOptions.RemoveEmptyEntries); - - Log.Cyan($"Specified triple slash locations:"); - foreach (string dirPath in splittedDirPaths) - { - DirectoryInfo dirInfo = new DirectoryInfo(dirPath); - if (!dirInfo.Exists) - { - Log.LogErrorAndExit($"This triple slash xml directory does not exist: {dirPath}"); - } - - config.DirsTripleSlashXmls.Add(dirInfo); - Log.Info($" - {dirPath}"); - } - - mode = Mode.Initial; - break; - } - - default: - { - Log.LogErrorPrintHelpAndExit("Unexpected mode."); - break; - } - } - } - - if (mode != Mode.Initial) - { - Log.LogErrorPrintHelpAndExit("You missed an argument value."); - } - - if (config.DirsDocsXml == null) - { - Log.LogErrorPrintHelpAndExit($"You must specify a path to the dotnet-api-docs xml folder with {nameof(Docs)}."); - } - - if (config.DirsTripleSlashXmls.Count == 0) - { - Log.LogErrorPrintHelpAndExit($"You must specify at least one triple slash xml folder path with {nameof(TripleSlash)}."); - } - - if (config.IncludedAssemblies.Count == 0) - { - Log.LogErrorPrintHelpAndExit($"You must specify at least one assembly with {nameof(IncludedAssemblies)}."); - } - - return config; - } - - // Tries to parse the user argument string as boolean, and if it fails, exits the program. - private static bool ParseOrExit(string arg, string paramFriendlyName) - { - if (!bool.TryParse(arg, out bool value)) - { - Log.LogErrorAndExit($"Invalid boolean value for '{paramFriendlyName}' argument: {arg}"); - } - - Log.Cyan($"{paramFriendlyName}:"); - Log.Info($" - {value}"); - - return value; - } - } -} diff --git a/DocsPortingTool/Docs/APIKind.cs b/DocsPortingTool/Docs/APIKind.cs deleted file mode 100644 index 5c2d7c9..0000000 --- a/DocsPortingTool/Docs/APIKind.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace DocsPortingTool.Docs -{ - public enum APIKind - { - Type, - Member - } -} diff --git a/DocsPortingTool/Docs/DocsAPI.cs b/DocsPortingTool/Docs/DocsAPI.cs deleted file mode 100644 index 9b7b19f..0000000 --- a/DocsPortingTool/Docs/DocsAPI.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public abstract class DocsAPI : IDocsAPI - { - private string? _docIdEscaped = null; - private List? _params; - private List? _parameters; - private List? _typeParameters; - private List? _typeParams; - private List? _assemblyInfos; - - protected readonly XElement XERoot; - - protected DocsAPI(XElement xeRoot) => XERoot = xeRoot; - - public abstract bool Changed { get; set; } - public string FilePath { get; set; } = string.Empty; - public abstract string DocId { get; } - - /// - /// The Parameter elements found inside the Parameters section. - /// - public List Parameters - { - get - { - if (_parameters == null) - { - XElement xeParameters = XERoot.Element("Parameters"); - if (xeParameters != null) - { - _parameters = xeParameters.Elements("Parameter").Select(x => new DocsParameter(x)).ToList(); - } - else - { - _parameters = new List(); - } - } - return _parameters; - } - } - - /// - /// The TypeParameter elements found inside the TypeParameters section. - /// - public List TypeParameters - { - get - { - if (_typeParameters == null) - { - XElement xeTypeParameters = XERoot.Element("TypeParameters"); - if (xeTypeParameters != null) - { - _typeParameters = xeTypeParameters.Elements("TypeParameter").Select(x => new DocsTypeParameter(x)).ToList(); - } - else - { - _typeParameters = new List(); - } - } - return _typeParameters; - } - } - - public XElement Docs - { - get - { - return XERoot.Element("Docs"); - } - } - - /// - /// 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; - } - } - - /// - /// 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 List(); - } - } - return _typeParams; - } - } - - public abstract string Summary { get; set; } - - public abstract string Remarks { get; set; } - - public List AssemblyInfos - { - get - { - if (_assemblyInfos == null) - { - _assemblyInfos = new List(); - } - return _assemblyInfos; - } - } - - public string DocIdEscaped - { - get - { - if (_docIdEscaped == null) - { - _docIdEscaped = DocId.Replace("<", "{").Replace(">", "}").Replace("<", "{").Replace(">", "}"); - } - return _docIdEscaped; - } - } - - public DocsParam SaveParam(XElement xeTripleSlashParam) - { - XElement xeDocsParam = new XElement(xeTripleSlashParam.Name); - xeDocsParam.ReplaceAttributes(xeTripleSlashParam.Attributes()); - XmlHelper.SaveFormattedAsXml(xeDocsParam, xeTripleSlashParam.Value); - DocsParam docsParam = new DocsParam(this, xeDocsParam); - Changed = true; - return docsParam; - } - - public APIKind Kind - { - get - { - return this switch - { - DocsMember _ => APIKind.Member, - DocsType _ => APIKind.Type, - _ => throw new ArgumentException("Unrecognized IDocsAPI object") - }; - } - } - - public DocsTypeParam AddTypeParam(string name, string value) - { - XElement typeParam = new XElement("typeparam"); - typeParam.SetAttributeValue("name", name); - XmlHelper.AddChildFormattedAsXml(Docs, typeParam, value); - Changed = true; - return new DocsTypeParam(this, typeParam); - } - - 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; - } - } - - // 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) - { - element = Docs.Element(name); - - if (element == null && addIfMissing) - { - element = new XElement(name); - XmlHelper.AddChildFormattedAsXml(Docs, element, Configuration.ToBeAdded); - } - - return element != null; - } - } -} diff --git a/DocsPortingTool/Docs/DocsAssemblyInfo.cs b/DocsPortingTool/Docs/DocsAssemblyInfo.cs deleted file mode 100644 index 81f4180..0000000 --- a/DocsPortingTool/Docs/DocsAssemblyInfo.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public class DocsAssemblyInfo - { - private readonly XElement XEAssemblyInfo; - public string AssemblyName - { - get - { - return 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 DocsAssemblyInfo(XElement xeAssemblyInfo) - { - XEAssemblyInfo = xeAssemblyInfo; - } - - public override string ToString() => AssemblyName; - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsAttribute.cs b/DocsPortingTool/Docs/DocsAttribute.cs deleted file mode 100644 index b4111b4..0000000 --- a/DocsPortingTool/Docs/DocsAttribute.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public 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 DocsAttribute(XElement xeAttribute) - { - XEAttribute = xeAttribute; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsCommentsContainer.cs b/DocsPortingTool/Docs/DocsCommentsContainer.cs deleted file mode 100644 index 6bc2ab8..0000000 --- a/DocsPortingTool/Docs/DocsCommentsContainer.cs +++ /dev/null @@ -1,303 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Xml; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public class DocsCommentsContainer - { - private Configuration Config { get; set; } - - private XDocument? xDoc = null; - - public readonly List Types = new List(); - public readonly List Members = new List(); - - public DocsCommentsContainer(Configuration config) - { - Config = config; - } - - public void CollectFiles() - { - Log.Info("Looking for Docs xml files..."); - - foreach (FileInfo fileInfo in EnumerateFiles()) - { - LoadFile(fileInfo); - } - - Log.Success("Finished looking for Docs xml files."); - Log.Line(); - } - - public void Save() - { - if (!Config.Save) - { - Log.Line(); - Log.Error("[No files were saved]"); - Log.Warning($"Did you forget to specify '-{nameof(Config.Save)} true'?"); - Log.Line(); - - return; - } - - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - Encoding encoding = Encoding.GetEncoding(1252); // Preserves original xml encoding from Docs repo - - List savedFiles = new List(); - foreach (var type in Types.Where(x => x.Changed)) - { - Log.Warning(false, $"Saving changes for {type.FilePath}:"); - - try - { - StreamReader sr = new StreamReader(type.FilePath); - int x = sr.Read(); // Force the first read to be done so the encoding is detected - sr.Close(); - - // These settings prevent the addition of the element on the first line and will preserve indentation+endlines - XmlWriterSettings xws = new XmlWriterSettings - { - OmitXmlDeclaration = true, - Indent = true, - Encoding = encoding, - CheckCharacters = false - }; - - using (XmlWriter xw = XmlWriter.Create(type.FilePath, xws)) - { - type.XDoc.Save(xw); - } - - // Workaround to delete the annoying endline added by XmlWriter.Save - string fileData = File.ReadAllText(type.FilePath); - if (!fileData.EndsWith(Environment.NewLine)) - { - File.WriteAllText(type.FilePath, fileData + Environment.NewLine, encoding); - } - - Log.Success(" [Saved]"); - } - catch (Exception e) - { - Log.Error(e.Message); - Log.Line(); - Log.Error(e.StackTrace ?? string.Empty); - if (e.InnerException != null) - { - Log.Line(); - Log.Error(e.InnerException.Message); - Log.Line(); - Log.Error(e.InnerException.StackTrace ?? string.Empty); - } - System.Threading.Thread.Sleep(1000); - } - - Log.Line(); - } - } - - private bool HasAllowedDirName(DirectoryInfo dirInfo) - { - return !Configuration.ForbiddenDirectories.Contains(dirInfo.Name) && !dirInfo.Name.EndsWith(".Tests"); - } - - private bool HasAllowedFileName(FileInfo fileInfo) - { - return !fileInfo.Name.StartsWith("ns-") && - fileInfo.Name != "index.xml" && - fileInfo.Name != "_filter.xml"; - } - - private IEnumerable EnumerateFiles() - { - var includedAssembliesAndNamespaces = Config.IncludedAssemblies.Concat(Config.IncludedNamespaces); - var excludedAssembliesAndNamespaces = Config.ExcludedAssemblies.Concat(Config.ExcludedNamespaces); - - foreach (DirectoryInfo rootDir in Config.DirsDocsXml) - { - // Try to find folders with the names of assemblies AND namespaces (if the user specified any) - foreach (string included in includedAssembliesAndNamespaces) - { - // If the user specified a sub-assembly or sub-namespace to exclude, we need to skip it - if (excludedAssembliesAndNamespaces.Any(excluded => included.StartsWith(excluded))) - { - continue; - } - - foreach (DirectoryInfo subDir in rootDir.EnumerateDirectories($"{included}*", SearchOption.TopDirectoryOnly)) - { - if (HasAllowedDirName(subDir)) - { - foreach (FileInfo fileInfo in subDir.EnumerateFiles("*.xml", SearchOption.AllDirectories)) - { - if (HasAllowedFileName(fileInfo)) - { - // LoadFile will determine if the Type is allowed or not - yield return fileInfo; - } - } - } - } - - if (!Config.SkipInterfaceImplementations) - { - // Find interfaces only inside System.* folders. - // Including Microsoft.* folders reaches the max limit of files to include in a list, plus there are no essential interfaces there. - foreach (DirectoryInfo subDir in rootDir.EnumerateDirectories("System*", SearchOption.AllDirectories)) - { - if (!Configuration.ForbiddenDirectories.Contains(subDir.Name) && - // Exclude any folder that starts with the excluded assemblies OR excluded namespaces - !excludedAssembliesAndNamespaces.Any(excluded => subDir.Name.StartsWith(excluded)) && !subDir.Name.EndsWith(".Tests")) - { - // Ensure including interface files that start with I and then an uppercase letter, and prevent including files like 'Int' - foreach (FileInfo fileInfo in subDir.EnumerateFiles("I*.xml", SearchOption.AllDirectories)) - { - if (fileInfo.Name[1] >= 'A' || fileInfo.Name[1] <= 'Z') - { - yield return fileInfo; - } - } - } - } - } - } - } - } - - private void LoadFile(FileInfo fileInfo) - { - if (!fileInfo.Exists) - { - Log.Error($"Docs xml file does not exist: {fileInfo.FullName}"); - return; - } - - xDoc = XDocument.Load(fileInfo.FullName); - - if (IsXmlMalformed(xDoc, fileInfo.FullName)) - { - return; - } - - DocsType docsType = new DocsType(fileInfo.FullName, xDoc, xDoc.Root); - - bool add = false; - bool addedAsInterface = false; - - bool containsForbiddenAssembly = docsType.AssemblyInfos.Any(assemblyInfo => - Config.ExcludedAssemblies.Any(excluded => assemblyInfo.AssemblyName.StartsWith(excluded)) || - Config.ExcludedNamespaces.Any(excluded => assemblyInfo.AssemblyName.StartsWith(excluded))); - - if (!containsForbiddenAssembly) - { - // If it's an interface, always add it if the user wants to detect EIIs, - // even if it's in an assembly that was not included but was not explicitly excluded - addedAsInterface = false; - if (!Config.SkipInterfaceImplementations) - { - // Interface files start with I, and have an 2nd alphabetic character - addedAsInterface = docsType.Name.Length >= 2 && docsType.Name[0] == 'I' && docsType.Name[1] >= 'A' && docsType.Name[1] <= 'Z'; - add |= addedAsInterface; - - } - - bool containsAllowedAssembly = docsType.AssemblyInfos.Any(assemblyInfo => - Config.IncludedAssemblies.Any(included => assemblyInfo.AssemblyName.StartsWith(included)) || - Config.IncludedNamespaces.Any(included => assemblyInfo.AssemblyName.StartsWith(included))); - - if (containsAllowedAssembly) - { - // If it was already added above as an interface, skip this part - // Otherwise, find out if the type belongs to the included assemblies, and if specified, to the included (and not excluded) types - // This includes interfaces even if user wants to skip EIIs - They will be added if they belong to this namespace or to the list of - // included (and not exluded) types, but will not be used for EII, but rather as normal types whose comments should be ported - if (!addedAsInterface) - { - // Either the user didn't specify namespace filtering (allow all namespaces) or specified particular ones to include/exclude - if (!Config.IncludedNamespaces.Any() || - (Config.IncludedNamespaces.Any(included => docsType.Namespace.StartsWith(included)) && - !Config.ExcludedNamespaces.Any(excluded => docsType.Namespace.StartsWith(excluded)))) - { - // Can add if the user didn't specify type filtering (allow all types), or specified particular ones to include/exclude - add = !Config.IncludedTypes.Any() || - (Config.IncludedTypes.Contains(docsType.Name) && - !Config.ExcludedTypes.Contains(docsType.Name)); - } - } - } - } - - if (add) - { - int totalMembersAdded = 0; - Types.Add(docsType); - - if (XmlHelper.TryGetChildElement(xDoc.Root, "Members", out XElement? xeMembers) && xeMembers != null) - { - foreach (XElement xeMember in xeMembers.Elements("Member")) - { - DocsMember member = new DocsMember(fileInfo.FullName, docsType, xeMember); - totalMembersAdded++; - Members.Add(member); - } - } - - string message = $"Type {docsType.DocId} added with {totalMembersAdded} member(s) included."; - if (addedAsInterface) - { - Log.Magenta("[Interface] - " + message); - } - else if (totalMembersAdded == 0) - { - Log.Warning(message); - } - else - { - Log.Success(message); - } - } - } - - private bool IsXmlMalformed(XDocument xDoc, string fileName) - { - if (xDoc.Root == null) - { - Log.Error($"Docs xml file does not have a root element: {fileName}"); - return true; - } - - if (xDoc.Root.Name == "Namespace") - { - Log.Error($"Skipping namespace file (should have been filtered already): {fileName}"); - return true; - } - - if (xDoc.Root.Name != "Type") - { - Log.Error($"Docs xml file does not have a 'Type' root element: {fileName}"); - return true; - } - - if (!xDoc.Root.HasElements) - { - Log.Error($"Docs xml file Type element does not have any children: {fileName}"); - return true; - } - - if (xDoc.Root.Elements("Docs").Count() != 1) - { - Log.Error($"Docs xml file Type element does not have a Docs child: {fileName}"); - return true; - } - - return false; - } - } -} diff --git a/DocsPortingTool/Docs/DocsException.cs b/DocsPortingTool/Docs/DocsException.cs deleted file mode 100644 index 68efc75..0000000 --- a/DocsPortingTool/Docs/DocsException.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public class DocsException - { - private readonly XElement XEException; - - public IDocsAPI ParentAPI - { - get; private set; - } - - public string Cref - { - get - { - return XmlHelper.GetAttributeValue(XEException, "cref"); - } - } - - public string Value - { - get - { - return XmlHelper.GetNodesInPlainText(XEException); - } - private set - { - XmlHelper.SaveFormattedAsXml(XEException, value); - } - } - - public string OriginalValue { get; private set; } - - public DocsException(IDocsAPI parentAPI, XElement xException) - { - ParentAPI = parentAPI; - XEException = 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 tripleSlashValue, int threshold) - { - Dictionary hashTripleSlash = GetHash(tripleSlashValue); - Dictionary hashDocs = GetHash(Value); - - int collisions = 0; - // Iterate all the words of the triple slash exception string - foreach (KeyValuePair word in hashTripleSlash) - { - // 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 triple slash - // 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)hashTripleSlash.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; - } - } -} diff --git a/DocsPortingTool/Docs/DocsMember.cs b/DocsPortingTool/Docs/DocsMember.cs deleted file mode 100644 index baedc1d..0000000 --- a/DocsPortingTool/Docs/DocsMember.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public class DocsMember : DocsAPI - { - private string? _memberName; - private List? _memberSignatures; - private string? _docId; - private List? _altMemberCref; - private List? _exceptions; - - public DocsMember(string filePath, DocsType parentType, XElement xeMember) - : base(xeMember) - { - FilePath = filePath; - ParentType = parentType; - AssemblyInfos.AddRange(XERoot.Elements("AssemblyInfo").Select(x => new DocsAssemblyInfo(x))); - } - - public DocsType ParentType { get; private set; } - - public override bool Changed - { - get => ParentType.Changed; - set => ParentType.Changed |= value; - } - - 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 override string DocId - { - get - { - if (_docId == null) - { - _docId = string.Empty; - DocsMemberSignature? ms = MemberSignatures.FirstOrDefault(x => x.Language == "DocId"); - if (ms == null) - { - string message = string.Format("Could not find a DocId MemberSignature for '{0}'", MemberName); - Log.Error(message); - throw new MissingMemberException(message); - } - _docId = ms.Value; - } - return _docId; - } - } - - public string MemberType - { - get - { - return XmlHelper.GetChildElementValue(XERoot, "MemberType"); - } - } - - public string ImplementsInterfaceMember - { - get - { - XElement xeImplements = XERoot.Element("Implements"); - if (xeImplements != null) - { - return XmlHelper.GetChildElementValue(xeImplements, "InterfaceMember"); - } - return string.Empty; - } - } - - public string ReturnType - { - get - { - XElement xeReturnValue = XERoot.Element("ReturnValue"); - if (xeReturnValue != null) - { - return XmlHelper.GetChildElementValue(xeReturnValue, "ReturnType"); - } - return string.Empty; - } - } - - public 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: {DocIdEscaped}"); - } - } - } - - public override string Summary - { - get - { - return GetNodesInPlainText("summary"); - } - set - { - SaveFormattedAsXml("summary", value, addIfMissing: true); - } - } - - public override string Remarks - { - get - { - return GetNodesInPlainText("remarks"); - } - set - { - SaveFormattedAsMarkdown("remarks", value, addIfMissing: !Analyzer.IsEmpty(value), isMember: true); - } - } - - 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: {DocIdEscaped}"); - } - } - } - - public List AltMemberCref - { - get - { - if (_altMemberCref == null) - { - if (Docs != null) - { - _altMemberCref = Docs.Elements("altmember").Select(x => XmlHelper.GetAttributeValue(x, "cref")).ToList(); - } - else - { - _altMemberCref = new List(); - } - } - return _altMemberCref; - } - } - - public List Exceptions - { - get - { - if (_exceptions == null) - { - if (Docs != null) - { - _exceptions = Docs.Elements("exception").Select(x => new DocsException(this, x)).ToList(); - } - else - { - _exceptions = new List(); - } - } - return _exceptions; - } - } - - public override string ToString() - { - return DocId; - } - - public DocsException AddException(string cref, string value) - { - XElement exception = new XElement("exception"); - exception.SetAttributeValue("cref", cref); - XmlHelper.AddChildFormattedAsXml(Docs, exception, value); - Changed = true; - return new DocsException(this, exception); - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsMemberSignature.cs b/DocsPortingTool/Docs/DocsMemberSignature.cs deleted file mode 100644 index 3fbdf66..0000000 --- a/DocsPortingTool/Docs/DocsMemberSignature.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public class DocsMemberSignature - { - private readonly XElement XEMemberSignature; - - public string Language - { - get - { - return XmlHelper.GetAttributeValue(XEMemberSignature, "Language"); - } - } - - public string Value - { - get - { - return XmlHelper.GetAttributeValue(XEMemberSignature, "Value"); - } - } - - public DocsMemberSignature(XElement xeMemberSignature) - { - XEMemberSignature = xeMemberSignature; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsParam.cs b/DocsPortingTool/Docs/DocsParam.cs deleted file mode 100644 index 7ec3a1a..0000000 --- a/DocsPortingTool/Docs/DocsParam.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public 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 DocsParam(IDocsAPI parentAPI, XElement xeDocsParam) - { - ParentAPI = parentAPI; - XEDocsParam = xeDocsParam; - } - public override string ToString() - { - return Name; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsParameter.cs b/DocsPortingTool/Docs/DocsParameter.cs deleted file mode 100644 index c9dd4bf..0000000 --- a/DocsPortingTool/Docs/DocsParameter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public 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; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsType.cs b/DocsPortingTool/Docs/DocsType.cs deleted file mode 100644 index 5628635..0000000 --- a/DocsPortingTool/Docs/DocsType.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - /// - /// Represents the root xml element (unique) of a Docs xml file, called Type. - /// - public class DocsType : DocsAPI - { - private string? _name; - private string? _fullName; - private string? _namespace; - private string? _docId; - private string? _baseTypeName; - private List? _interfaceNames; - private List? _attributes; - private List? _typesSignatures; - - public DocsType(string filePath, XDocument xDoc, XElement xeRoot) - : base(xeRoot) - { - FilePath = filePath; - XDoc = xDoc; - AssemblyInfos.AddRange(XERoot.Elements("AssemblyInfo").Select(x => new DocsAssemblyInfo(x))); - } - - public XDocument XDoc { get; set; } - - public override bool Changed { get; set; } - - public string Name - { - get - { - if (_name == null) - { - _name = XmlHelper.GetAttributeValue(XERoot, "Name"); - } - return _name; - } - } - - public string FullName - { - get - { - if (_fullName == null) - { - _fullName = XmlHelper.GetAttributeValue(XERoot, "FullName"); - } - return _fullName; - } - } - - public string Namespace - { - get - { - if (_namespace == null) - { - int lastDotPosition = FullName.LastIndexOf('.'); - _namespace = lastDotPosition < 0 ? FullName : FullName.Substring(0, lastDotPosition); - } - return _namespace; - } - } - - public List TypeSignatures - { - get - { - if (_typesSignatures == null) - { - _typesSignatures = XERoot.Elements("TypeSignature").Select(x => new DocsTypeSignature(x)).ToList(); - } - return _typesSignatures; - } - } - - public override string DocId - { - get - { - if (_docId == null) - { - DocsTypeSignature? dts = TypeSignatures.FirstOrDefault(x => x.Language == "DocId"); - if (dts == null) - { - string message = $"DocId TypeSignature not found for FullName"; - Log.Error($"DocId TypeSignature not found for FullName"); - throw new MissingMemberException(message); - } - _docId = dts.Value; - } - return _docId; - } - } - - public XElement Base - { - get - { - return XERoot.Element("Base"); - } - } - - public string BaseTypeName - { - get - { - if (_baseTypeName == null) - { - _baseTypeName = XmlHelper.GetChildElementValue(Base, "BaseTypeName"); - } - return _baseTypeName; - } - } - - public XElement Interfaces - { - get - { - return XERoot.Element("Interfaces"); - } - } - - public List InterfaceNames - { - get - { - if (_interfaceNames == null) - { - _interfaceNames = Interfaces.Elements("Interface").Select(x => XmlHelper.GetChildElementValue(x, "InterfaceName")).ToList(); - } - return _interfaceNames; - } - } - - public List Attributes - { - get - { - if (_attributes == null) - { - XElement e = XERoot.Element("Attributes"); - _attributes = (e != null) ? e.Elements("Attribute").Select(x => new DocsAttribute(x)).ToList() : new List(); - } - return _attributes; - } - } - - public override string Summary - { - get - { - return GetNodesInPlainText("summary"); - } - set - { - SaveFormattedAsXml("summary", value, addIfMissing: true); - } - } - - public override string Remarks - { - get - { - return GetNodesInPlainText("remarks"); - } - set - { - SaveFormattedAsMarkdown("remarks", value, addIfMissing: !Analyzer.IsEmpty(value), isMember: false); - } - } - - public override string ToString() - { - return FullName; - } - } -} diff --git a/DocsPortingTool/Docs/DocsTypeParam.cs b/DocsPortingTool/Docs/DocsTypeParam.cs deleted file mode 100644 index 08790e3..0000000 --- a/DocsPortingTool/Docs/DocsTypeParam.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Threading; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - /// - /// Each one of these typeparam objects live inside the Docs section inside the Member object. - /// - public class DocsTypeParam - { - private readonly XElement XEDocsTypeParam; - public IDocsAPI ParentAPI - { - get; private set; - } - public string Name - { - get - { - return XmlHelper.GetAttributeValue(XEDocsTypeParam, "name"); - } - } - public string Value - { - get - { - return XmlHelper.GetNodesInPlainText(XEDocsTypeParam); - } - set - { - XmlHelper.SaveFormattedAsXml(XEDocsTypeParam, value); - ParentAPI.Changed = true; - } - } - - public DocsTypeParam(IDocsAPI parentAPI, XElement xeDocsTypeParam) - { - ParentAPI = parentAPI; - XEDocsTypeParam = xeDocsTypeParam; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsTypeParameter.cs b/DocsPortingTool/Docs/DocsTypeParameter.cs deleted file mode 100644 index d32482e..0000000 --- a/DocsPortingTool/Docs/DocsTypeParameter.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - /// - /// Each one of these TypeParameter objects islocated inside the TypeParameters section inside the Member. - /// - public class DocsTypeParameter - { - private readonly XElement XETypeParameter; - public string Name - { - get - { - return XmlHelper.GetAttributeValue(XETypeParameter, "Name"); - } - } - private XElement? Constraints - { - get - { - return XETypeParameter.Element("Constraints"); - } - } - private List? _constraintsParamterAttributes; - public List ConstraintsParameterAttributes - { - get - { - if (_constraintsParamterAttributes == null) - { - if (Constraints != null) - { - _constraintsParamterAttributes = Constraints.Elements("ParameterAttribute").Select(x => XmlHelper.GetNodesInPlainText(x)).ToList(); - } - else - { - _constraintsParamterAttributes = new List(); - } - } - return _constraintsParamterAttributes; - } - } - - public string ConstraintsBaseTypeName - { - get - { - if (Constraints != null) - { - return XmlHelper.GetChildElementValue(Constraints, "BaseTypeName"); - } - return string.Empty; - } - } - - public DocsTypeParameter(XElement xeTypeParameter) - { - XETypeParameter = xeTypeParameter; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/DocsTypeSignature.cs b/DocsPortingTool/Docs/DocsTypeSignature.cs deleted file mode 100644 index 97fa67c..0000000 --- a/DocsPortingTool/Docs/DocsTypeSignature.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public class DocsTypeSignature - { - private readonly XElement XETypeSignature; - - public string Language - { - get - { - return XmlHelper.GetAttributeValue(XETypeSignature, "Language"); - } - } - - public string Value - { - get - { - return XmlHelper.GetAttributeValue(XETypeSignature, "Value"); - } - } - - public DocsTypeSignature(XElement xeTypeSignature) - { - XETypeSignature = xeTypeSignature; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Docs/IDocsAPI.cs b/DocsPortingTool/Docs/IDocsAPI.cs deleted file mode 100644 index 71cf16a..0000000 --- a/DocsPortingTool/Docs/IDocsAPI.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using System.Xml.Linq; - -namespace DocsPortingTool.Docs -{ - public interface IDocsAPI - { - public abstract APIKind Kind { get; } - public abstract bool Changed { get; set; } - public abstract string FilePath { get; set; } - public abstract string DocId { get; } - public abstract XElement Docs { get; } - public abstract List Parameters { get; } - public abstract List Params { get; } - public abstract List TypeParameters { get; } - public abstract List TypeParams { get; } - public abstract string Summary { get; set; } - public abstract string Remarks { get; set; } - public abstract DocsParam SaveParam(XElement xeCoreFXParam); - public abstract DocsTypeParam AddTypeParam(string name, string value); - } -} diff --git a/DocsPortingTool/DocsPortingTool.cs b/DocsPortingTool/DocsPortingTool.cs deleted file mode 100644 index 494a5a7..0000000 --- a/DocsPortingTool/DocsPortingTool.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DocsPortingTool -{ - public static class DocsPortingTool - { - public static void Main(string[] args) - { - Configuration config = Configuration.GetFromCommandLineArguments(args); - Analyzer analyzer = new Analyzer(config); - analyzer.Start(); - } - } -} diff --git a/DocsPortingTool/DocsPortingTool.csproj b/DocsPortingTool/DocsPortingTool.csproj deleted file mode 100644 index a75771d..0000000 --- a/DocsPortingTool/DocsPortingTool.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - Exe - net5.0 - - - Microsoft - carlossanlop - enable - true - true - - - - - - diff --git a/DocsPortingTool/Extensions.cs b/DocsPortingTool/Extensions.cs deleted file mode 100644 index fdcb90d..0000000 --- a/DocsPortingTool/Extensions.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; - -namespace DocsPortingTool -{ - // Provides generic extension methods. - public static class Extensions - { - // Adds a string to a list of strings if the element is not there yet. The method makes sure to escape unexpected curly brackets to prevent formatting exceptions. - public static void AddIfNotExists(this List list, string element) - { - string cleanedElement = element.Escaped(); - if (!list.Contains(cleanedElement)) - { - list.Add(cleanedElement); - } - } - - // Removes the specified subtrings from another string - public static string RemoveSubstrings(this string oldString, params string[] stringsToRemove) - { - string newString = oldString; - foreach (string toRemove in stringsToRemove) - { - if (newString.Contains(toRemove)) - { - newString = newString.Replace(toRemove, string.Empty); - } - } - return newString; - } - - // Some API DocIDs with types contain "{" and "}" to enclose the typeparam, which causes - // an exception to be thrown when trying to embed the string in a formatted string. - public static string Escaped(this string str) => str.Replace("{", "{{").Replace("}", "}}"); - } - -} diff --git a/DocsPortingTool/Log.cs b/DocsPortingTool/Log.cs deleted file mode 100644 index 3c29b9a..0000000 --- a/DocsPortingTool/Log.cs +++ /dev/null @@ -1,377 +0,0 @@ -using System; - -namespace DocsPortingTool -{ - public class Log - { - private static void WriteLine(string format, params object[]? args) - { - if (args == null || args.Length == 0) - { - Console.WriteLine(format); - } - else - { - Console.WriteLine(format, args); - } - } - - private static void Write(string format, params object[]? args) - { - if (args == null || args.Length == 0) - { - Console.Write(format); - } - else - { - Console.Write(format, args); - } - } - - public static void Print(bool endline, ConsoleColor foregroundColor, string format, params object[]? args) - { - ConsoleColor initialColor = Console.ForegroundColor; - Console.ForegroundColor = foregroundColor; - if (endline) - { - WriteLine(format, args); - } - else - { - Write(format, args); - } - Console.ForegroundColor = initialColor; - } - - public static void Info(string format) - { - Info(format, null); - } - - public static void Info(string format, params object[]? args) - { - Info(true, format, args); - } - - public static void Info(bool endline, string format, params object[]? args) - { - Print(endline, ConsoleColor.White, format, args); - } - - public static void Success(string format) - { - Success(format, null); - } - - public static void Success(string format, params object[]? args) - { - Success(true, format, args); - } - - public static void Success(bool endline, string format, params object[]? args) - { - Print(endline, ConsoleColor.Green, format, args); - } - - public static void Warning(string format) - { - Warning(format, null); - } - - public static void Warning(string format, params object[]? args) - { - Warning(true, format, args); - } - - public static void Warning(bool endline, string format, params object[]? args) - { - Print(endline, ConsoleColor.Yellow, format, args); - } - - public static void Error(string format) - { - Error(format, null); - } - - public static void Error(string format, params object[]? args) - { - Error(true, format, args); - } - - public static void Error(bool endline, string format, params object[]? args) - { - Print(endline, ConsoleColor.Red, format, args); - } - - public static void Cyan(string format) - { - Cyan(format, null); - } - - public static void Cyan(string format, params object[]? args) - { - Cyan(true, format, args); - } - - public static void Magenta(bool endline, string format, params object[]? args) - { - Print(endline, ConsoleColor.Magenta, format, args); - } - - public static void Magenta(string format) - { - Magenta(format, null); - } - - public static void Magenta(string format, params object[]? args) - { - Magenta(true, format, args); - } - - public static void Cyan(bool endline, string format, params object[]? args) - { - Print(endline, ConsoleColor.Cyan, format, args); - } - - public static void Assert(bool condition, string format, params object[]? args) - { - Assert(true, condition, format, args); - } - - public static void Assert(bool endline, bool condition, string format, params object[]? args) - { - if (condition) - { - Success(endline, format, args); - } - else - { - Error(endline, format, args); - } - } - - public static void Line() - { - Console.WriteLine(); - } - - public delegate void PrintHelpFunction(); - - public static void LogErrorAndExit(string format, params object[]? args) - { - Error(format, args); - Environment.Exit(0); - } - - public static void LogErrorPrintHelpAndExit(string format, params object[]? args) - { - Error(format, args); - PrintHelp(); - Environment.Exit(0); - } - - public static void PrintHelp() - { - Cyan(@" -This tool finds and ports triple slash comments found in .NET repos but do not yet exist in the dotnet-api-docs repo. - -The instructions below assume %SourceRepos% is the root folder of all your git cloned projects. - -Options: - - - MANDATORY - ------------------------------------------------------------ - | PARAMETER | TYPE | DESCRIPTION | - ------------------------------------------------------------ - - -Docs folder path The absolute directory root path where the Docs xml files are located. - Known locations: - > Runtime: %SourceRepos%\dotnet-api-docs\xml - > WPF: %SourceRepos%\dotnet-api-docs\xml - > WinForms: %SourceRepos%\dotnet-api-docs\xml - > ASP.NET MVC: %SourceRepos%\AspNetApiDocs\aspnet-mvc\xml - > ASP.NET Core: %SourceRepos%\AspNetApiDocs\aspnet-core\xml - Usage example: - -Docs %SourceRepos%\dotnet-api-docs\xml,%SourceRepos%\AspNetApiDocs\aspnet-mvc\xml - - -TripleSlash comma-separated A comma separated list (no spaces) of absolute directory paths where we should recursively - folder paths look for triple slash comment xml files. - Known locations: - > Runtime: %SourceRepos%\runtime\artifacts\bin\ - > CoreCLR: %SourceRepos%\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release\IL\ - > WinForms: %SourceRepos%\winforms\artifacts\bin\ - > WPF: %SourceRepos%\wpf\.tools\native\bin\dotnet-api-docs_netcoreapp3.0\0.0.0.1\_intellisense\netcore-3.0\ - Usage example: - -TripleSlash %SourceRepos%\corefx\artifacts\bin\,%SourceRepos%\winforms\artifacts\bin\ - - -IncludedAssemblies string list Comma separated list (no spaces) of assemblies to include. - Usage example: - -IncludedAssemblies System.IO,System.Runtime - - IMPORTANT: - Namespaces usually match the assembly name. There are some exceptions, like with types that live in - the System.Runtime assembly. For those cases, make sure to also specify the -IncludedNamespaces argument. - - - OPTIONAL - ------------------------------------------------------------ - | PARAMETER | TYPE | DESCRIPTION | - ------------------------------------------------------------ - - -h | -Help no arguments Displays this help message. If used, nothing else is processed and the program exits. - - -DisablePrompts bool Default is false (prompts are disabled). - Avoids prompting the user for input to correct some particular errors. - Usage example: - -DisablePrompts true - - -ExceptionCollisionThreshold int (0-100) Default is 70 (If >=70% of words collide, the string is not ported). - Decides how sensitive the detection of existing exception strings should be. - The tool compares the Docs exception string with the Triple Slash exception string. - If the number of words found in the Docs exception is below the specified threshold, - then the Triple Slash string is appended at the end of the Docs string. - The user is expected to verify the value. - The reason for this is that exceptions go through language review, and may contain more - than one root cause (separated by '-or-'), and there is no easy way to know if the string - has already been ported or not. - Usage example: - -ExceptionCollisionThreshold 60 - - -ExcludedAssemblies string list Default is empty (does not ignore any assemblies/namespaces). - Comma separated list (no spaces) of specific .NET assemblies/namespaces to ignore. - Usage example: - -ExcludedAssemblies System.IO.Compression,System.IO.Pipes - - -ExcludedNamespaces string list Default is empty (does not exclude any namespaces from the specified assemblies). - Comma separated list (no spaces) of specific namespaces to exclude from the specified assemblies. - Usage example: - -ExcludedNamespaces System.Runtime.Intrinsics,System.Reflection.Metadata - - -ExcludedTypes string list Default is empty (does not ignore any types). - Comma separated list (no spaces) of names of types to ignore. - Usage example: - -ExcludedTypes ArgumentException,Stream - - -IncludedNamespaces string list Default is empty (includes all namespaces from the specified assemblies). - Comma separated list (no spaces) of specific namespaces to include from the specified assemblies. - Usage example: - -IncludedNamespaces System,System.Data - - -IncludedTypes string list Default is empty (includes all types in the desired assemblies/namespaces). - Comma separated list (no spaces) of specific types to include. - Usage example: - -IncludedTypes FileStream,DirectoryInfo - - -PortExceptionsExisting bool Default is false (does not find and append existing exceptions). - Enable or disable finding, porting and appending summaries from existing exceptions. - Setting this to true can result in a lot of noise because there is - no easy way to detect if an exception summary has been ported already or not, - especially after it went through language review. - See `-ExceptionCollisionThreshold` to set the collision sensitivity. - Usage example: - -PortExceptionsExisting true - - -PortExceptionsNew bool Default is true (ports new exceptions). - Enable or disable finding and porting new exceptions. - Usage example: - -PortExceptionsNew false - - -PortMemberParams bool Default is true (ports Member parameters). - Enable or disable finding and porting Member parameters. - Usage example: - -PortMemberParams false - - -PortMemberProperties bool Default is true (ports Member properties). - Enable or disable finding and porting Member properties. - Usage example: - -PortMemberProperties false - - -PortMemberReturns bool Default is true (ports Member return values). - Enable or disable finding and porting Member return values. - Usage example: - -PortMemberReturns false - - -PortMemberRemarks bool Default is true (ports Member remarks). - Enable or disable finding and porting Member remarks. - Usage example: - -PortMemberRemarks false - - -PortMemberSummaries bool Default is true (ports Member summaries). - Enable or disable finding and porting Member summaries. - Usage example: - -PortMemberSummaries false - - -PortMemberTypeParams bool Default is true (ports Member TypeParams). - Enable or disable finding and porting Member TypeParams. - Usage example: - -PortMemberTypeParams false - - -PortTypeParams bool Default is true (ports Type Params). - Enable or disable finding and porting Type Params. - Usage example: - -PortTypeParams false - - -PortTypeRemarks bool Default is true (ports Type remarks). - Enable or disable finding and porting Type remarks. - Usage example: - -PortTypeRemarks false - - -PortTypeSummaries bool Default is true (ports Type summaries). - Enable or disable finding and porting Type summaries. - Usage example: - -PortTypeSummaries false - - -PortTypeTypeParams bool Default is true (ports Type TypeParams). - Enable or disable finding and porting Type TypeParams. - Usage example: - -PortTypeTypeParams false - - -PrintUndoc bool Default is false (prints a basic summary). - Prints a detailed summary of all the docs APIs that are undocumented. - Usage example: - -PrintUndoc true - - -Save bool Default is false (does not save changes). - Whether you want to save the changes in the dotnet-api-docs xml files. - Usage example: - -Save true - - -SkipInterfaceImplementations bool Default is false (includes interface implementations). - Whether you want the original interface documentation to be considered to fill the - undocumented API's documentation when the API itself does not provide its own documentation. - Setting this to false will include Explicit Interface Implementations as well. - Usage example: - -SkipInterfaceImplementations true - - -SkipInterfaceRemarks bool Default is true (excludes appending interface remarks). - Whether you want interface implementation remarks to be used when the API itself has no remarks. - Very noisy and generally the content in those remarks do not apply to the API that implements - the interface API. - Usage example: - -SkipInterfaceRemarks false - - "); - Warning(@" - tl;dr: Just specify these parameters: - - -Docs - -TripleSlash [,,...,] - -IncludedAssemblies [,,...] - -Save true - - Example: - DocsPortingTool \ - -Docs D:\dotnet-api-docs\xml \ - -TripleSlash D:\runtime\artifacts\bin \ - -IncludedAssemblies System.IO.FileSystem,System.Runtime.Intrinsics \ - -Save true -"); - Magenta(@" - Note: - If the names of your assemblies differ from the namespaces wheres your APIs live, specify the -IncludedNamespaces argument too. - - "); - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/Properties/launchSettings.json b/DocsPortingTool/Properties/launchSettings.json deleted file mode 100644 index da4e252..0000000 --- a/DocsPortingTool/Properties/launchSettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "profiles": { - "DocsPortingTool": { - "commandName": "Project", - "commandLineArgs": "-TripleSlash D:\\runtime\\artifacts\\bin,D:\\runtime\\artifacts\\bin\\coreclr\\Windows_NT.x64.Release\\IL -Docs D:\\dotnet-api-docs\\xml -Save false -SkipInterfaceImplementations true -IncludedAssemblies System.Private.CoreLib -IncludedNamespaces System.Threading.Tasks -IncludedTypes Tasks", - "environmentVariables": { - "DOCS_IOT": "D:\\iot\\artifacts\\bin", - "DOCS_CORECLR": "D:\\runtime\\artifacts\\bin\\coreclr\\Windows_NT.x64.Release\\IL\\", - "DOCS_WINFORMS": "D:\\winforms\\artifacts\\bin\\", - "DOCS_WPF": "D:\\wpf\\.tools\\native\\bin\\dotnet-api-docs_netcoreapp3.0\\0.0.0.1\\_intellisense\\\\netcore-3.0\\", - "DOCS_RUNTIME": "D:\\runtime\\artifacts\\bin\\" - } - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/TripleSlash/TripleSlashCommentsContainer.cs b/DocsPortingTool/TripleSlash/TripleSlashCommentsContainer.cs deleted file mode 100644 index 8223a48..0000000 --- a/DocsPortingTool/TripleSlash/TripleSlashCommentsContainer.cs +++ /dev/null @@ -1,186 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Xml.Linq; - -/* -The triple slash comments xml files for... -A) corefx are saved in: - corefx/artifacts/bin/ -B) coreclr are saved in: - coreclr\packages\microsoft.netcore.app\\ref\netcoreapp\ - or in: - corefx/artifacts/bin/docs - but in this case, only namespaces found in coreclr/src/System.Private.CoreLib/shared need to be searched here. - -Each xml file represents a namespace. -The files are structured like this: - -root - assembly (1) - name (1) - members (many) - member(0:M) - summary (0:1) - param (0:M) - returns (0:1) - exception (0:M) - Note: The exception value may contain xml nodes. -*/ -namespace DocsPortingTool.TripleSlash -{ - public class TripleSlashCommentsContainer - { - private Configuration Config { get; set; } - - private XDocument? xDoc = null; - - public List Members = new List(); - - public int TotalFiles - { - get - { - return Members.Count; - } - } - - public TripleSlashCommentsContainer(Configuration config) - { - Config = config; - } - - public void CollectFiles() - { - Log.Info("Looking for triple slash xml files..."); - - foreach (FileInfo fileInfo in EnumerateFiles()) - { - LoadFile(fileInfo, printSuccess: true); - } - - Log.Success("Finished looking for triple slash xml files."); - Log.Line(); - } - - private IEnumerable EnumerateFiles() - { - foreach (DirectoryInfo dirInfo in Config.DirsTripleSlashXmls) - { - // 1) Find all the xml files inside all the subdirectories inside the triple slash xml directory - foreach (DirectoryInfo subDir in dirInfo.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)) - { - if (!Configuration.ForbiddenDirectories.Contains(subDir.Name) && !subDir.Name.EndsWith(".Tests")) - { - foreach (FileInfo fileInfo in subDir.EnumerateFiles("*.xml", SearchOption.AllDirectories)) - { - yield return fileInfo; - } - } - } - - // 2) Find all the xml files in the top directory - foreach (FileInfo fileInfo in dirInfo.EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly)) - { - yield return fileInfo; - } - } - } - - private void LoadFile(FileInfo fileInfo, bool printSuccess) - { - if (!fileInfo.Exists) - { - Log.Error($"Triple slash xml file does not exist: {fileInfo.FullName}"); - return; - } - - xDoc = XDocument.Load(fileInfo.FullName); - - if (IsXmlMalformed(xDoc, fileInfo.FullName, out string? assembly)) - { - return; - } - - int totalAdded = 0; - if (XmlHelper.TryGetChildElement(xDoc.Root, "members", out XElement? xeMembers) && xeMembers != null) - { - foreach (XElement xeMember in xeMembers.Elements("member")) - { - TripleSlashMember member = new TripleSlashMember(xeMember, assembly); - - if (Config.IncludedAssemblies.Any(included => member.Assembly.StartsWith(included)) && - !Config.ExcludedAssemblies.Any(excluded => member.Assembly.StartsWith(excluded))) - { - // No namespaces provided by the user means they want to port everything from that assembly - if (!Config.IncludedNamespaces.Any() || - (Config.IncludedNamespaces.Any(included => member.Namespace.StartsWith(included)) && - !Config.ExcludedNamespaces.Any(excluded => member.Namespace.StartsWith(excluded)))) - { - totalAdded++; - Members.Add(member); - } - } - } - } - - if (printSuccess && totalAdded > 0) - { - Log.Success($"{totalAdded} triple slash member(s) added from xml file '{fileInfo.FullName}'"); - } - } - - private bool IsXmlMalformed(XDocument xDoc, string fileName, [NotNullWhen(returnValue: false)] out string? assembly) - { - assembly = null; - - if (xDoc.Root == null) - { - Log.Error($"Triple slash xml file does not contain a root element: {fileName}"); - return true; - } - - if (xDoc.Root.Name != "doc") - { - Log.Error($"Triple slash xml file does not contain a doc element: {fileName}"); - return true; - } - - if (!xDoc.Root.HasElements) - { - Log.Error($"Triple slash xml file doc element not have any children: {fileName}"); - return true; - } - - if (xDoc.Root.Elements("assembly").Count() != 1) - { - Log.Error($"Triple slash xml file does not contain exactly 1 'assembly' element: {fileName}"); - return true; - } - - if (xDoc.Root.Elements("members").Count() != 1) - { - Log.Error($"Triple slash xml file does not contain exactly 1 'members' element: {fileName}"); - return true; - } - - XElement xAssembly = xDoc.Root.Element("assembly"); - if (xAssembly.Elements("name").Count() != 1) - { - Log.Error($"Triple slash xml file assembly element does not contain exactly 1 'name' element: {fileName}"); - return true; - } - - assembly = xAssembly.Element("name").Value; - if (string.IsNullOrEmpty(assembly)) - { - Log.Error($"Triple slash xml file assembly string is null or empty: {fileName}"); - return true; - } - - return false; - } - } -} diff --git a/DocsPortingTool/TripleSlash/TripleSlashException.cs b/DocsPortingTool/TripleSlash/TripleSlashException.cs deleted file mode 100644 index 0adba3e..0000000 --- a/DocsPortingTool/TripleSlash/TripleSlashException.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.TripleSlash -{ - public class TripleSlashException - { - public XElement XEException - { - get; - private set; - } - - private string _cref = string.Empty; - public string Cref - { - get - { - if (string.IsNullOrWhiteSpace(_cref)) - { - _cref = XmlHelper.GetAttributeValue(XEException, "cref"); - } - return _cref; - } - } - - private string _value = string.Empty; - public string Value - { - get - { - if (string.IsNullOrWhiteSpace(_value)) - { - _value = XmlHelper.GetNodesInPlainText(XEException); - } - return _value; - } - } - - public TripleSlashException(XElement xeException) - { - XEException = xeException; - } - - public override string ToString() - { - return $"{Cref} - {Value}"; - } - } -} diff --git a/DocsPortingTool/TripleSlash/TripleSlashMember.cs b/DocsPortingTool/TripleSlash/TripleSlashMember.cs deleted file mode 100644 index 1036af0..0000000 --- a/DocsPortingTool/TripleSlash/TripleSlashMember.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace DocsPortingTool.TripleSlash -{ - public class TripleSlashMember - { - private readonly XElement XEMember; - - public string Assembly { get; private set; } - - - private string _namespace = string.Empty; - public string Namespace - { - get - { - if (string.IsNullOrWhiteSpace(_namespace)) - { - string[] splittedParenthesis = Name.Split('(', StringSplitOptions.RemoveEmptyEntries); - string withoutParenthesisAndPrefix = splittedParenthesis[0][2..]; // Exclude the "X:" prefix - string[] splittedDots = withoutParenthesisAndPrefix.Split('.', StringSplitOptions.RemoveEmptyEntries); - - _namespace = string.Join('.', splittedDots.Take(splittedDots.Length - 1)); - } - - return _namespace; - } - } - - private string? _name; - /// - /// The API DocId. - /// - public string Name - { - get - { - if (_name == null) - { - _name = XmlHelper.GetAttributeValue(XEMember, "name"); - } - return _name; - } - } - - private List? _params; - public List Params - { - get - { - if (_params == null) - { - _params = XEMember.Elements("param").Select(x => new TripleSlashParam(x)).ToList(); - } - return _params; - } - } - - private List? _typeParams; - public List TypeParams - { - get - { - if (_typeParams == null) - { - _typeParams = XEMember.Elements("typeparam").Select(x => new TripleSlashTypeParam(x)).ToList(); - } - return _typeParams; - } - } - - private List? _exceptions; - public IEnumerable Exceptions - { - get - { - if (_exceptions == null) - { - _exceptions = XEMember.Elements("exception").Select(x => new TripleSlashException(x)).ToList(); - } - return _exceptions; - } - } - - private string? _summary; - public string Summary - { - get - { - if (_summary == null) - { - XElement xElement = XEMember.Element("summary"); - _summary = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; - } - return _summary; - } - } - - public string? _value; - public string Value - { - get - { - if (_value == null) - { - XElement xElement = XEMember.Element("value"); - _value = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; - } - return _value; - } - } - - private string? _returns; - public string Returns - { - get - { - if (_returns == null) - { - XElement xElement = XEMember.Element("returns"); - _returns = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; - } - return _returns; - } - } - - private string? _remarks; - public string Remarks - { - get - { - if (_remarks == null) - { - XElement xElement = XEMember.Element("remarks"); - _remarks = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; - } - return _remarks; - } - } - - public TripleSlashMember(XElement xeMember, string assembly) - { - if (string.IsNullOrEmpty(assembly)) - { - throw new ArgumentNullException(nameof(assembly)); - } - - XEMember = xeMember ?? throw new ArgumentNullException(nameof(xeMember)); - Assembly = assembly; - } - - public override string ToString() - { - return Name; - } - } -} diff --git a/DocsPortingTool/TripleSlash/TripleSlashParam.cs b/DocsPortingTool/TripleSlash/TripleSlashParam.cs deleted file mode 100644 index f8ef49f..0000000 --- a/DocsPortingTool/TripleSlash/TripleSlashParam.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.TripleSlash -{ - public class TripleSlashParam - { - public XElement XEParam - { - get; - private set; - } - - private string _name = string.Empty; - public string Name - { - get - { - if (string.IsNullOrWhiteSpace(_name)) - { - _name = XmlHelper.GetAttributeValue(XEParam, "name"); - } - return _name; - } - } - - private string _value = string.Empty; - public string Value - { - get - { - if (string.IsNullOrWhiteSpace(_value)) - { - _value = XmlHelper.GetNodesInPlainText(XEParam); - } - return _value; - } - } - - public TripleSlashParam(XElement xeParam) - { - XEParam = xeParam; - } - } -} diff --git a/DocsPortingTool/TripleSlash/TripleSlashTypeParam.cs b/DocsPortingTool/TripleSlash/TripleSlashTypeParam.cs deleted file mode 100644 index 510cc95..0000000 --- a/DocsPortingTool/TripleSlash/TripleSlashTypeParam.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Xml.Linq; - -namespace DocsPortingTool.TripleSlash -{ - public class TripleSlashTypeParam - { - public XElement XETypeParam; - - private string _name = string.Empty; - public string Name - { - get - { - if (string.IsNullOrWhiteSpace(_name)) - { - _name = XmlHelper.GetAttributeValue(XETypeParam, "name"); - } - return _name; - } - } - - private string _value = string.Empty; - public string Value - { - get - { - if (string.IsNullOrWhiteSpace(_value)) - { - _value = XmlHelper.GetNodesInPlainText(XETypeParam); - } - return _value; - } - } - - public TripleSlashTypeParam(XElement xeTypeParam) - { - XETypeParam = xeTypeParam; - } - } -} \ No newline at end of file diff --git a/DocsPortingTool/XmlHelper.cs b/DocsPortingTool/XmlHelper.cs deleted file mode 100644 index 4b6effc..0000000 --- a/DocsPortingTool/XmlHelper.cs +++ /dev/null @@ -1,319 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using System.Xml; -using System.Xml.Linq; - -namespace DocsPortingTool -{ - public 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" }, - }; - - public static string GetAttributeValue(XElement parent, string name) - { - if (parent == null) - { - Log.Error("A null parent was passed when attempting to get attribute '{0}'", name); - throw new ArgumentNullException(nameof(parent)); - } - else - { - XAttribute attr = parent.Attribute(name); - if (attr != null) - { - return attr.Value.Trim(); - } - } - return string.Empty; - } - - public static bool TryGetChildElement(XElement parent, string name, out XElement? child) - { - child = null; - - if (parent == null || string.IsNullOrWhiteSpace(name)) - return false; - - child = parent.Element(name); - - return child != null; - } - - public static string GetChildElementValue(XElement parent, string childName) - { - XElement child = parent.Element(childName); - - if (child != null) - { - return GetNodesInPlainText(child); - } - - return string.Empty; - } - - public static string GetNodesInPlainText(XElement element) - { - if (element == null) - { - Log.Error("A null element was passed when attempting to retrieve the nodes in plain text."); - throw new ArgumentNullException(nameof(element)); - } - return string.Join("", element.Nodes()).Trim(); - } - - public static void SaveFormattedAsMarkdown(XElement element, string newValue, bool isMember) - { - if (element == null) - { - Log.Error("A null element was passed when attempting to save formatted as markdown"); - throw new ArgumentNullException(nameof(element)); - } - - // 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 = RemoveUndesiredEndlines(newValue); - updatedValue = SubstituteRemarksRegexPatterns(updatedValue); - updatedValue = ReplaceMarkdownPatterns(updatedValue); - - 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) - { - Log.Error("A null parent was passed when attempting to add child formatted as markdown"); - throw new ArgumentNullException(nameof(parent)); - } - - if (child == null) - { - Log.Error("A null child was passed when attempting to add child formatted as markdown"); - throw new ArgumentNullException(nameof(child)); - } - - SaveFormattedAsMarkdown(child, childValue, isMember); - parent.Add(child); - } - - public static void SaveFormattedAsXml(XElement element, string newValue, bool removeUndesiredEndlines = true) - { - if (element == null) - { - Log.Error("A null element was passed when attempting to save formatted as xml"); - throw new ArgumentNullException(nameof(element)); - } - - 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) - { - Log.Error("A null element was passed when attempting to append formatted as xml"); - throw new ArgumentNullException(nameof(element)); - } - - SaveFormattedAsXml(element, GetNodesInPlainText(element) + valueToAppend, removeUndesiredEndlines); - } - - public static void AddChildFormattedAsXml(XElement parent, XElement child, string childValue) - { - if (parent == null) - { - Log.Error("A null parent was passed when attempting to add child formatted as xml"); - throw new ArgumentNullException(nameof(parent)); - } - - if (child == null) - { - Log.Error("A null child was passed when attempting to add child formatted as xml"); - throw new ArgumentNullException(nameof(child)); - } - - SaveFormattedAsXml(child, childValue); - parent.Add(child); - } - - private static string RemoveUndesiredEndlines(string value) - { - Regex regex = new Regex(@"((?'undesiredEndlinePrefix'[^\.\:])(\r\n)+[ \t]*)"); - string newValue = value; - if (regex.IsMatch(value)) - { - newValue = regex.Replace(value, @"${undesiredEndlinePrefix} "); - } - return newValue.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 (updatedValue.Contains(kvp.Key)) - { - updatedValue = updatedValue.Replace(kvp.Key, kvp.Value); - } - } - 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); - } - } - 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; - } - - private static string SubstituteRegexPatterns(string value, Dictionary replaceableRegexPatterns) - { - foreach (KeyValuePair pattern in replaceableRegexPatterns) - { - Regex regex = new Regex(pattern.Key); - if (regex.IsMatch(value)) - { - value = regex.Replace(value, pattern.Value); - } - } - - return value; - } - } -} \ No newline at end of file From 2d6972fd6207b91a37bb74c2166140bcdaef7218 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:41:58 -0800 Subject: [PATCH 30/65] Docs changes. --- Libraries/Docs/APIKind.cs | 8 + Libraries/Docs/DocsAPI.cs | 261 ++++++++++++++++++++ Libraries/Docs/DocsAssemblyInfo.cs | 38 +++ Libraries/Docs/DocsAttribute.cs | 29 +++ Libraries/Docs/DocsCommentsContainer.cs | 308 ++++++++++++++++++++++++ Libraries/Docs/DocsException.cs | 104 ++++++++ Libraries/Docs/DocsMember.cs | 224 +++++++++++++++++ Libraries/Docs/DocsMemberSignature.cs | 30 +++ Libraries/Docs/DocsParam.cs | 41 ++++ Libraries/Docs/DocsParameter.cs | 27 +++ Libraries/Docs/DocsSeeAlso.cs | 34 +++ Libraries/Docs/DocsType.cs | 199 +++++++++++++++ Libraries/Docs/DocsTypeParam.cs | 44 ++++ Libraries/Docs/DocsTypeParameter.cs | 64 +++++ Libraries/Docs/DocsTypeSignature.cs | 30 +++ Libraries/Docs/IDocsAPI.cs | 22 ++ 16 files changed, 1463 insertions(+) create mode 100644 Libraries/Docs/APIKind.cs create mode 100644 Libraries/Docs/DocsAPI.cs create mode 100644 Libraries/Docs/DocsAssemblyInfo.cs create mode 100644 Libraries/Docs/DocsAttribute.cs create mode 100644 Libraries/Docs/DocsCommentsContainer.cs create mode 100644 Libraries/Docs/DocsException.cs create mode 100644 Libraries/Docs/DocsMember.cs create mode 100644 Libraries/Docs/DocsMemberSignature.cs create mode 100644 Libraries/Docs/DocsParam.cs create mode 100644 Libraries/Docs/DocsParameter.cs create mode 100644 Libraries/Docs/DocsSeeAlso.cs create mode 100644 Libraries/Docs/DocsType.cs create mode 100644 Libraries/Docs/DocsTypeParam.cs create mode 100644 Libraries/Docs/DocsTypeParameter.cs create mode 100644 Libraries/Docs/DocsTypeSignature.cs create mode 100644 Libraries/Docs/IDocsAPI.cs diff --git a/Libraries/Docs/APIKind.cs b/Libraries/Docs/APIKind.cs new file mode 100644 index 0000000..00f554e --- /dev/null +++ b/Libraries/Docs/APIKind.cs @@ -0,0 +1,8 @@ +namespace Libraries.Docs +{ + internal enum APIKind + { + Type, + Member + } +} diff --git a/Libraries/Docs/DocsAPI.cs b/Libraries/Docs/DocsAPI.cs new file mode 100644 index 0000000..86eead0 --- /dev/null +++ b/Libraries/Docs/DocsAPI.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal abstract class DocsAPI : IDocsAPI + { + private string? _docIdEscaped = null; + private List? _params; + private List? _parameters; + private List? _typeParameters; + private List? _typeParams; + private List? _assemblyInfos; + private List? _seeAlsos; + + protected readonly XElement XERoot; + + protected DocsAPI(XElement xeRoot) => XERoot = xeRoot; + + public abstract bool Changed { get; set; } + public string FilePath { get; set; } = string.Empty; + public abstract string DocId { get; } + + /// + /// The Parameter elements found inside the Parameters section. + /// + public List Parameters + { + get + { + if (_parameters == null) + { + XElement? xeParameters = XERoot.Element("Parameters"); + if (xeParameters == null) + { + _parameters = new(); + } + else + { + _parameters = xeParameters.Elements("Parameter").Select(x => new DocsParameter(x)).ToList(); + } + } + return _parameters; + } + } + + /// + /// The TypeParameter elements found inside the TypeParameters section. + /// + public List TypeParameters + { + get + { + if (_typeParameters == null) + { + XElement? xeTypeParameters = XERoot.Element("TypeParameters"); + if (xeTypeParameters == null) + { + _typeParameters = new(); + } + else + { + _typeParameters = xeTypeParameters.Elements("TypeParameter").Select(x => new DocsTypeParameter(x)).ToList(); + } + } + return _typeParameters; + } + } + + public XElement Docs + { + get + { + return XERoot.Element("Docs"); + } + } + + /// + /// 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; + } + } + + /// + /// 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 SeeAlsos + { + get + { + if (_seeAlsos == null) + { + if (Docs != null) + { + _seeAlsos = Docs.Elements("seealso").Select(x => new DocsSeeAlso(this, x)).ToList(); + } + else + { + _seeAlsos = new(); + } + } + return _seeAlsos; + } + } + + public abstract string Summary { get; set; } + + public abstract string Remarks { get; set; } + + public List AssemblyInfos + { + get + { + if (_assemblyInfos == null) + { + _assemblyInfos = new List(); + } + return _assemblyInfos; + } + } + + public string DocIdEscaped + { + get + { + if (_docIdEscaped == null) + { + _docIdEscaped = DocId.Replace("<", "{").Replace(">", "}").Replace("<", "{").Replace(">", "}"); + } + return _docIdEscaped; + } + } + + 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 APIKind Kind + { + get + { + return this switch + { + DocsMember _ => APIKind.Member, + DocsType _ => APIKind.Type, + _ => throw new ArgumentException("Unrecognized IDocsAPI object") + }; + } + } + + public DocsTypeParam AddTypeParam(string name, string value) + { + XElement typeParam = new XElement("typeparam"); + typeParam.SetAttributeValue("name", name); + XmlHelper.AddChildFormattedAsXml(Docs, typeParam, value); + Changed = true; + return new DocsTypeParam(this, typeParam); + } + + 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; + } + } + + // 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) + { + element = null; + + if (Docs == null) + { + return false; + } + + element = Docs.Element(name); + + if (element == null && addIfMissing) + { + element = new XElement(name); + XmlHelper.AddChildFormattedAsXml(Docs, element, Configuration.ToBeAdded); + } + + return element != null; + } + } +} diff --git a/Libraries/Docs/DocsAssemblyInfo.cs b/Libraries/Docs/DocsAssemblyInfo.cs new file mode 100644 index 0000000..7840315 --- /dev/null +++ b/Libraries/Docs/DocsAssemblyInfo.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsAssemblyInfo + { + private readonly XElement XEAssemblyInfo; + public string AssemblyName + { + get + { + return 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 DocsAssemblyInfo(XElement xeAssemblyInfo) + { + XEAssemblyInfo = xeAssemblyInfo; + } + + public override string ToString() => AssemblyName; + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsAttribute.cs b/Libraries/Docs/DocsAttribute.cs new file mode 100644 index 0000000..a07ad42 --- /dev/null +++ b/Libraries/Docs/DocsAttribute.cs @@ -0,0 +1,29 @@ +using System.Xml.Linq; + +namespace Libraries.Docs +{ + 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 DocsAttribute(XElement xeAttribute) + { + XEAttribute = xeAttribute; + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsCommentsContainer.cs b/Libraries/Docs/DocsCommentsContainer.cs new file mode 100644 index 0000000..0d9fc44 --- /dev/null +++ b/Libraries/Docs/DocsCommentsContainer.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsCommentsContainer + { + private Configuration Config { get; set; } + + private XDocument? xDoc = null; + + public readonly List Types = new List(); + public readonly List Members = new List(); + + public DocsCommentsContainer(Configuration config) + { + Config = config; + } + + public void CollectFiles() + { + Log.Info("Looking for Docs xml files..."); + + foreach (FileInfo fileInfo in EnumerateFiles()) + { + LoadFile(fileInfo); + } + + Log.Success("Finished looking for Docs xml files."); + Log.Line(); + } + + public void Save() + { + if (!Config.Save) + { + Log.Line(); + Log.Error("[No files were saved]"); + Log.Warning($"Did you forget to specify '-{nameof(Config.Save)} true'?"); + Log.Line(); + + return; + } + + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + Encoding encoding = Encoding.GetEncoding(1252); // Preserves original xml encoding from Docs repo + + List savedFiles = new List(); + foreach (var type in Types.Where(x => x.Changed)) + { + Log.Warning(false, $"Saving changes for {type.FilePath}:"); + + try + { + StreamReader sr = new StreamReader(type.FilePath); + int x = sr.Read(); // Force the first read to be done so the encoding is detected + sr.Close(); + + // These settings prevent the addition of the element on the first line and will preserve indentation+endlines + XmlWriterSettings xws = new XmlWriterSettings + { + OmitXmlDeclaration = true, + Indent = true, + Encoding = encoding, + CheckCharacters = false + }; + + using (XmlWriter xw = XmlWriter.Create(type.FilePath, xws)) + { + type.XDoc.Save(xw); + } + + // Workaround to delete the annoying endline added by XmlWriter.Save + string fileData = File.ReadAllText(type.FilePath); + if (!fileData.EndsWith(Environment.NewLine)) + { + File.WriteAllText(type.FilePath, fileData + Environment.NewLine, encoding); + } + + Log.Success(" [Saved]"); + } + catch (Exception e) + { + Log.Error(e.Message); + Log.Line(); + Log.Error(e.StackTrace ?? string.Empty); + if (e.InnerException != null) + { + Log.Line(); + Log.Error(e.InnerException.Message); + Log.Line(); + Log.Error(e.InnerException.StackTrace ?? string.Empty); + } + System.Threading.Thread.Sleep(1000); + } + + Log.Line(); + } + } + + private bool HasAllowedDirName(DirectoryInfo dirInfo) + { + return !Configuration.ForbiddenBinSubdirectories.Contains(dirInfo.Name) && !dirInfo.Name.EndsWith(".Tests"); + } + + private bool HasAllowedFileName(FileInfo fileInfo) + { + return !fileInfo.Name.StartsWith("ns-") && + fileInfo.Name != "index.xml" && + fileInfo.Name != "_filter.xml"; + } + + private IEnumerable EnumerateFiles() + { + var includedAssembliesAndNamespaces = Config.IncludedAssemblies.Concat(Config.IncludedNamespaces); + var excludedAssembliesAndNamespaces = Config.ExcludedAssemblies.Concat(Config.ExcludedNamespaces); + + foreach (DirectoryInfo rootDir in Config.DirsDocsXml) + { + // Try to find folders with the names of assemblies AND namespaces (if the user specified any) + foreach (string included in includedAssembliesAndNamespaces) + { + // If the user specified a sub-assembly or sub-namespace to exclude, we need to skip it + if (excludedAssembliesAndNamespaces.Any(excluded => included.StartsWith(excluded))) + { + continue; + } + + foreach (DirectoryInfo subDir in rootDir.EnumerateDirectories($"{included}*", SearchOption.TopDirectoryOnly)) + { + if (HasAllowedDirName(subDir)) + { + foreach (FileInfo fileInfo in subDir.EnumerateFiles("*.xml", SearchOption.AllDirectories)) + { + if (HasAllowedFileName(fileInfo)) + { + // LoadFile will determine if the Type is allowed or not + yield return fileInfo; + } + } + } + } + + if (!Config.SkipInterfaceImplementations) + { + // Find interfaces only inside System.* folders. + // Including Microsoft.* folders reaches the max limit of files to include in a list, plus there are no essential interfaces there. + foreach (DirectoryInfo subDir in rootDir.EnumerateDirectories("System*", SearchOption.AllDirectories)) + { + if (!Configuration.ForbiddenBinSubdirectories.Contains(subDir.Name) && + // Exclude any folder that starts with the excluded assemblies OR excluded namespaces + !excludedAssembliesAndNamespaces.Any(excluded => subDir.Name.StartsWith(excluded)) && !subDir.Name.EndsWith(".Tests")) + { + // Ensure including interface files that start with I and then an uppercase letter, and prevent including files like 'Int' + foreach (FileInfo fileInfo in subDir.EnumerateFiles("I*.xml", SearchOption.AllDirectories)) + { + if (fileInfo.Name[1] >= 'A' || fileInfo.Name[1] <= 'Z') + { + yield return fileInfo; + } + } + } + } + } + } + } + } + + private void LoadFile(FileInfo fileInfo) + { + if (!fileInfo.Exists) + { + Log.Error($"Docs xml file does not exist: {fileInfo.FullName}"); + return; + } + + xDoc = XDocument.Load(fileInfo.FullName); + + if (IsXmlMalformed(xDoc, fileInfo.FullName)) + { + return; + } + + DocsType docsType = new DocsType(fileInfo.FullName, xDoc, xDoc.Root!); + + bool add = false; + bool addedAsInterface = false; + + bool containsForbiddenAssembly = docsType.AssemblyInfos.Any(assemblyInfo => + Config.ExcludedAssemblies.Any(excluded => assemblyInfo.AssemblyName.StartsWith(excluded)) || + Config.ExcludedNamespaces.Any(excluded => assemblyInfo.AssemblyName.StartsWith(excluded))); + + if (!containsForbiddenAssembly) + { + // If it's an interface, always add it if the user wants to detect EIIs, + // even if it's in an assembly that was not included but was not explicitly excluded + addedAsInterface = false; + if (!Config.SkipInterfaceImplementations) + { + // Interface files start with I, and have an 2nd alphabetic character + addedAsInterface = docsType.Name.Length >= 2 && docsType.Name[0] == 'I' && docsType.Name[1] >= 'A' && docsType.Name[1] <= 'Z'; + add |= addedAsInterface; + + } + + bool containsAllowedAssembly = docsType.AssemblyInfos.Any(assemblyInfo => + Config.IncludedAssemblies.Any(included => assemblyInfo.AssemblyName.StartsWith(included)) || + Config.IncludedNamespaces.Any(included => assemblyInfo.AssemblyName.StartsWith(included))); + + if (containsAllowedAssembly) + { + // If it was already added above as an interface, skip this part + // Otherwise, find out if the type belongs to the included assemblies, and if specified, to the included (and not excluded) types + // This includes interfaces even if user wants to skip EIIs - They will be added if they belong to this namespace or to the list of + // included (and not exluded) types, but will not be used for EII, but rather as normal types whose comments should be ported + if (!addedAsInterface) + { + // Either the user didn't specify namespace filtering (allow all namespaces) or specified particular ones to include/exclude + if (!Config.IncludedNamespaces.Any() || + (Config.IncludedNamespaces.Any(included => docsType.Namespace.StartsWith(included)) && + !Config.ExcludedNamespaces.Any(excluded => docsType.Namespace.StartsWith(excluded)))) + { + // Can add if the user didn't specify type filtering (allow all types), or specified particular ones to include/exclude + add = !Config.IncludedTypes.Any() || + (Config.IncludedTypes.Contains(docsType.Name) && + !Config.ExcludedTypes.Contains(docsType.Name)); + } + } + } + } + + if (add) + { + int totalMembersAdded = 0; + Types.Add(docsType); + + if (XmlHelper.TryGetChildElement(xDoc.Root!, "Members", out XElement? xeMembers) && xeMembers != null) + { + foreach (XElement xeMember in xeMembers.Elements("Member")) + { + DocsMember member = new DocsMember(fileInfo.FullName, docsType, xeMember); + totalMembersAdded++; + Members.Add(member); + } + } + + string message = $"Type {docsType.DocId} added with {totalMembersAdded} member(s) included."; + if (addedAsInterface) + { + Log.Magenta("[Interface] - " + message); + } + else if (totalMembersAdded == 0) + { + Log.Warning(message); + } + else + { + Log.Success(message); + } + } + } + + private bool IsXmlMalformed(XDocument? xDoc, string fileName) + { + if(xDoc == null) + { + Log.Error($"XDocument is null: {fileName}"); + return true; + } + if (xDoc.Root == null) + { + Log.Error($"Docs xml file does not have a root element: {fileName}"); + return true; + } + + if (xDoc.Root.Name == "Namespace") + { + Log.Error($"Skipping namespace file (should have been filtered already): {fileName}"); + return true; + } + + if (xDoc.Root.Name != "Type") + { + Log.Error($"Docs xml file does not have a 'Type' root element: {fileName}"); + return true; + } + + if (!xDoc.Root.HasElements) + { + Log.Error($"Docs xml file Type element does not have any children: {fileName}"); + return true; + } + + if (xDoc.Root.Elements("Docs").Count() != 1) + { + Log.Error($"Docs xml file Type element does not have a Docs child: {fileName}"); + return true; + } + + return false; + } + } +} diff --git a/Libraries/Docs/DocsException.cs b/Libraries/Docs/DocsException.cs new file mode 100644 index 0000000..1450887 --- /dev/null +++ b/Libraries/Docs/DocsException.cs @@ -0,0 +1,104 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsException + { + private readonly XElement XEException; + + public IDocsAPI ParentAPI + { + get; private set; + } + + public string Cref + { + get + { + return XmlHelper.GetAttributeValue(XEException, "cref"); + } + } + + public string Value + { + get + { + return XmlHelper.GetNodesInPlainText(XEException); + } + private set + { + XmlHelper.SaveFormattedAsXml(XEException, value); + } + } + + public string OriginalValue { get; private set; } + + public DocsException(IDocsAPI parentAPI, XElement xException) + { + ParentAPI = parentAPI; + XEException = 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; + } + } +} diff --git a/Libraries/Docs/DocsMember.cs b/Libraries/Docs/DocsMember.cs new file mode 100644 index 0000000..1383004 --- /dev/null +++ b/Libraries/Docs/DocsMember.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsMember : DocsAPI + { + private string? _memberName; + private List? _memberSignatures; + private string? _docId; + private List? _altMemberCref; + private List? _exceptions; + + public DocsMember(string filePath, DocsType parentType, XElement xeMember) + : base(xeMember) + { + FilePath = filePath; + ParentType = parentType; + AssemblyInfos.AddRange(XERoot.Elements("AssemblyInfo").Select(x => new DocsAssemblyInfo(x))); + } + + public DocsType ParentType { get; private set; } + + public override bool Changed + { + get => ParentType.Changed; + set => ParentType.Changed |= value; + } + + 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 override string DocId + { + get + { + if (_docId == null) + { + _docId = string.Empty; + DocsMemberSignature? ms = MemberSignatures.FirstOrDefault(x => x.Language == "DocId"); + if (ms == null) + { + string message = string.Format("Could not find a DocId MemberSignature for '{0}'", MemberName); + Log.Error(message); + throw new MissingMemberException(message); + } + _docId = ms.Value; + } + return _docId; + } + } + + public string MemberType + { + get + { + return XmlHelper.GetChildElementValue(XERoot, "MemberType"); + } + } + + public string ImplementsInterfaceMember + { + get + { + XElement xeImplements = XERoot.Element("Implements"); + if (xeImplements != null) + { + return XmlHelper.GetChildElementValue(xeImplements, "InterfaceMember"); + } + return string.Empty; + } + } + + public string ReturnType + { + get + { + XElement xeReturnValue = XERoot.Element("ReturnValue"); + if (xeReturnValue != null) + { + return XmlHelper.GetChildElementValue(xeReturnValue, "ReturnType"); + } + return string.Empty; + } + } + + public 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: {DocIdEscaped}"); + } + } + } + + public override string Summary + { + get + { + return GetNodesInPlainText("summary"); + } + set + { + SaveFormattedAsXml("summary", value, addIfMissing: true); + } + } + + public override string Remarks + { + get + { + return GetNodesInPlainText("remarks"); + } + set + { + SaveFormattedAsMarkdown("remarks", value, addIfMissing: !value.IsDocsEmpty(), isMember: true); + } + } + + 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: {DocIdEscaped}"); + } + } + } + + public List AltMemberCref + { + get + { + if (_altMemberCref == null) + { + if (Docs != null) + { + _altMemberCref = Docs.Elements("altmember").Select(x => XmlHelper.GetAttributeValue(x, "cref")).ToList(); + } + else + { + _altMemberCref = new List(); + } + } + return _altMemberCref; + } + } + + public List Exceptions + { + get + { + if (_exceptions == null) + { + if (Docs != null) + { + _exceptions = Docs.Elements("exception").Select(x => new DocsException(this, x)).ToList(); + } + else + { + _exceptions = new List(); + } + } + return _exceptions; + } + } + + public override string ToString() + { + return DocId; + } + + public DocsException AddException(string cref, string value) + { + XElement exception = new XElement("exception"); + exception.SetAttributeValue("cref", cref); + XmlHelper.AddChildFormattedAsXml(Docs, exception, value); + Changed = true; + return new DocsException(this, exception); + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsMemberSignature.cs b/Libraries/Docs/DocsMemberSignature.cs new file mode 100644 index 0000000..f9eff57 --- /dev/null +++ b/Libraries/Docs/DocsMemberSignature.cs @@ -0,0 +1,30 @@ +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsMemberSignature + { + private readonly XElement XEMemberSignature; + + public string Language + { + get + { + return XmlHelper.GetAttributeValue(XEMemberSignature, "Language"); + } + } + + public string Value + { + get + { + return XmlHelper.GetAttributeValue(XEMemberSignature, "Value"); + } + } + + public DocsMemberSignature(XElement xeMemberSignature) + { + XEMemberSignature = xeMemberSignature; + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsParam.cs b/Libraries/Docs/DocsParam.cs new file mode 100644 index 0000000..c5a09b2 --- /dev/null +++ b/Libraries/Docs/DocsParam.cs @@ -0,0 +1,41 @@ +using System.Xml.Linq; + +namespace Libraries.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 DocsParam(IDocsAPI parentAPI, XElement xeDocsParam) + { + ParentAPI = parentAPI; + XEDocsParam = xeDocsParam; + } + public override string ToString() + { + return Name; + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsParameter.cs b/Libraries/Docs/DocsParameter.cs new file mode 100644 index 0000000..ec598b8 --- /dev/null +++ b/Libraries/Docs/DocsParameter.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; + +namespace Libraries.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; + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsSeeAlso.cs b/Libraries/Docs/DocsSeeAlso.cs new file mode 100644 index 0000000..ca218b8 --- /dev/null +++ b/Libraries/Docs/DocsSeeAlso.cs @@ -0,0 +1,34 @@ +#nullable enable +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsSeeAlso + { + private readonly XElement XESeeAlso; + + public IDocsAPI ParentAPI + { + get; private set; + } + + public string Cref + { + get + { + return XmlHelper.GetAttributeValue(XESeeAlso, "cref"); + } + } + + public DocsSeeAlso(IDocsAPI parentAPI, XElement xeSeeAlso) + { + ParentAPI = parentAPI; + XESeeAlso = xeSeeAlso; + } + + public override string ToString() + { + return $"{Cref}"; + } + } +} diff --git a/Libraries/Docs/DocsType.cs b/Libraries/Docs/DocsType.cs new file mode 100644 index 0000000..6731f87 --- /dev/null +++ b/Libraries/Docs/DocsType.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + /// + /// Represents the root xml element (unique) of a Docs xml file, called Type. + /// + internal class DocsType : DocsAPI + { + private string? _name; + private string? _fullName; + private string? _namespace; + private string? _docId; + private string? _baseTypeName; + private List? _interfaceNames; + private List? _attributes; + private List? _typesSignatures; + + public DocsType(string filePath, XDocument xDoc, XElement xeRoot) + : base(xeRoot) + { + FilePath = filePath; + XDoc = xDoc; + AssemblyInfos.AddRange(XERoot.Elements("AssemblyInfo").Select(x => new DocsAssemblyInfo(x))); + } + + public XDocument XDoc { get; set; } + + public override bool Changed { get; set; } + + public string Name + { + get + { + if (_name == null) + { + _name = XmlHelper.GetAttributeValue(XERoot, "Name"); + } + return _name; + } + } + + public string FullName + { + get + { + if (_fullName == null) + { + _fullName = XmlHelper.GetAttributeValue(XERoot, "FullName"); + } + return _fullName; + } + } + + public string Namespace + { + get + { + if (_namespace == null) + { + int lastDotPosition = FullName.LastIndexOf('.'); + _namespace = lastDotPosition < 0 ? FullName : FullName.Substring(0, lastDotPosition); + } + return _namespace; + } + } + + public List TypeSignatures + { + get + { + if (_typesSignatures == null) + { + _typesSignatures = XERoot.Elements("TypeSignature").Select(x => new DocsTypeSignature(x)).ToList(); + } + return _typesSignatures; + } + } + + public override string DocId + { + get + { + if (_docId == null) + { + DocsTypeSignature? dts = TypeSignatures.FirstOrDefault(x => x.Language == "DocId"); + if (dts == null) + { + string message = $"DocId TypeSignature not found for FullName"; + Log.Error($"DocId TypeSignature not found for FullName"); + throw new MissingMemberException(message); + } + _docId = dts.Value; + } + return _docId; + } + } + + public XElement? Base + { + get + { + return XERoot.Element("Base"); + } + } + + public string BaseTypeName + { + get + { + if (Base == null) + { + _baseTypeName = string.Empty; + } + else if (_baseTypeName == null) + { + _baseTypeName = XmlHelper.GetChildElementValue(Base, "BaseTypeName"); + } + return _baseTypeName; + } + } + + public XElement? Interfaces + { + get + { + return XERoot.Element("Interfaces"); + } + } + + public List InterfaceNames + { + get + { + if (Interfaces == null) + { + _interfaceNames = new(); + } + else if (_interfaceNames == null) + { + _interfaceNames = Interfaces.Elements("Interface").Select(x => XmlHelper.GetChildElementValue(x, "InterfaceName")).ToList(); + } + return _interfaceNames; + } + } + + public List Attributes + { + get + { + if (_attributes == null) + { + XElement? e = XERoot.Element("Attributes"); + if (e == null) + { + _attributes = new(); + } + else + { + _attributes = (e != null) ? e.Elements("Attribute").Select(x => new DocsAttribute(x)).ToList() : new List(); + } + } + return _attributes; + } + } + + public override string Summary + { + get + { + return GetNodesInPlainText("summary"); + } + set + { + SaveFormattedAsXml("summary", value, addIfMissing: true); + } + } + + public override string Remarks + { + get + { + return GetNodesInPlainText("remarks"); + } + set + { + SaveFormattedAsMarkdown("remarks", value, addIfMissing: !value.IsDocsEmpty(), isMember: false); + } + } + + public override string ToString() + { + return FullName; + } + } +} diff --git a/Libraries/Docs/DocsTypeParam.cs b/Libraries/Docs/DocsTypeParam.cs new file mode 100644 index 0000000..af2ea7a --- /dev/null +++ b/Libraries/Docs/DocsTypeParam.cs @@ -0,0 +1,44 @@ +#nullable enable +using System.Xml.Linq; + +namespace Libraries.Docs +{ + /// + /// Each one of these typeparam objects live inside the Docs section inside the Member object. + /// + internal class DocsTypeParam + { + private readonly XElement XEDocsTypeParam; + public IDocsAPI ParentAPI + { + get; private set; + } + + public string Name + { + get + { + return XmlHelper.GetAttributeValue(XEDocsTypeParam, "name"); + } + } + + public string Value + { + get + { + return XmlHelper.GetNodesInPlainText(XEDocsTypeParam); + } + set + { + XmlHelper.SaveFormattedAsXml(XEDocsTypeParam, value); + ParentAPI.Changed = true; + } + } + + public DocsTypeParam(IDocsAPI parentAPI, XElement xeDocsTypeParam) + { + ParentAPI = parentAPI; + XEDocsTypeParam = xeDocsTypeParam; + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsTypeParameter.cs b/Libraries/Docs/DocsTypeParameter.cs new file mode 100644 index 0000000..ac66251 --- /dev/null +++ b/Libraries/Docs/DocsTypeParameter.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + /// + /// Each one of these TypeParameter objects islocated inside the TypeParameters section inside the Member. + /// + internal class DocsTypeParameter + { + private readonly XElement XETypeParameter; + public string Name + { + get + { + return XmlHelper.GetAttributeValue(XETypeParameter, "Name"); + } + } + private XElement? Constraints + { + get + { + return XETypeParameter.Element("Constraints"); + } + } + private List? _constraintsParamterAttributes; + public List ConstraintsParameterAttributes + { + get + { + if (_constraintsParamterAttributes == null) + { + if (Constraints != null) + { + _constraintsParamterAttributes = Constraints.Elements("ParameterAttribute").Select(x => XmlHelper.GetNodesInPlainText(x)).ToList(); + } + else + { + _constraintsParamterAttributes = new List(); + } + } + return _constraintsParamterAttributes; + } + } + + public string ConstraintsBaseTypeName + { + get + { + if (Constraints != null) + { + return XmlHelper.GetChildElementValue(Constraints, "BaseTypeName"); + } + return string.Empty; + } + } + + public DocsTypeParameter(XElement xeTypeParameter) + { + XETypeParameter = xeTypeParameter; + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/DocsTypeSignature.cs b/Libraries/Docs/DocsTypeSignature.cs new file mode 100644 index 0000000..5ca5c46 --- /dev/null +++ b/Libraries/Docs/DocsTypeSignature.cs @@ -0,0 +1,30 @@ +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsTypeSignature + { + private readonly XElement XETypeSignature; + + public string Language + { + get + { + return XmlHelper.GetAttributeValue(XETypeSignature, "Language"); + } + } + + public string Value + { + get + { + return XmlHelper.GetAttributeValue(XETypeSignature, "Value"); + } + } + + public DocsTypeSignature(XElement xeTypeSignature) + { + XETypeSignature = xeTypeSignature; + } + } +} \ No newline at end of file diff --git a/Libraries/Docs/IDocsAPI.cs b/Libraries/Docs/IDocsAPI.cs new file mode 100644 index 0000000..837e371 --- /dev/null +++ b/Libraries/Docs/IDocsAPI.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal interface IDocsAPI + { + public abstract APIKind Kind { get; } + public abstract bool Changed { get; set; } + public abstract string FilePath { get; set; } + public abstract string DocId { get; } + public abstract XElement Docs { get; } + public abstract List Parameters { get; } + public abstract List Params { get; } + public abstract List TypeParameters { get; } + public abstract List TypeParams { get; } + public abstract string Summary { get; set; } + public abstract string Remarks { get; set; } + public abstract DocsParam SaveParam(XElement xeCoreFXParam); + public abstract DocsTypeParam AddTypeParam(string name, string value); + } +} From 36f65d714e3a65d292713b7dd004108de41d4eaf Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:42:16 -0800 Subject: [PATCH 31/65] IntelliSense XML rename and changes. --- .../IntelliSenseXmlCommentsContainer.cs | 190 ++++++++++++++++++ .../IntelliSenseXmlException.cs | 49 +++++ .../IntelliSenseXml/IntelliSenseXmlMember.cs | 160 +++++++++++++++ .../IntelliSenseXml/IntelliSenseXmlParam.cs | 44 ++++ .../IntelliSenseXmlTypeParam.cs | 40 ++++ 5 files changed, 483 insertions(+) create mode 100644 Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs create mode 100644 Libraries/IntelliSenseXml/IntelliSenseXmlException.cs create mode 100644 Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs create mode 100644 Libraries/IntelliSenseXml/IntelliSenseXmlParam.cs create mode 100644 Libraries/IntelliSenseXml/IntelliSenseXmlTypeParam.cs diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs new file mode 100644 index 0000000..9a167ba --- /dev/null +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs @@ -0,0 +1,190 @@ +#nullable enable +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +/* +The IntelliSense xml comments files for... +A) corefx are saved in: + corefx/artifacts/bin/ +B) coreclr are saved in: + coreclr\packages\microsoft.netcore.app\\ref\netcoreapp\ + or in: + corefx/artifacts/bin/docs + but in this case, only namespaces found in coreclr/src/System.Private.CoreLib/shared need to be searched here. + +Each xml file represents a namespace. +The files are structured like this: + +root + assembly (1) + name (1) + members (many) + member(0:M) + summary (0:1) + param (0:M) + returns (0:1) + exception (0:M) + Note: The exception value may contain xml nodes. +*/ +namespace Libraries.IntelliSenseXml +{ + internal class IntelliSenseXmlCommentsContainer + { + private Configuration Config { get; set; } + + private XDocument? xDoc = null; + + // The IntelliSense xml files do not separate types from members, like ECMA xml files do - Everything is a member. + public List Members = new List(); + + public IntelliSenseXmlCommentsContainer(Configuration config) + { + Config = config; + } + + public void CollectFiles() + { + Log.Info("Looking for IntelliSense xml files..."); + + foreach (FileInfo fileInfo in EnumerateFiles()) + { + LoadFile(fileInfo, printSuccess: true); + } + + Log.Success("Finished looking for IntelliSense xml files."); + Log.Line(); + } + + private IEnumerable EnumerateFiles() + { + foreach (DirectoryInfo dirInfo in Config.DirsIntelliSense) + { + // 1) Find all the xml files inside all the subdirectories inside the IntelliSense xml directory + foreach (DirectoryInfo subDir in dirInfo.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)) + { + if (!Configuration.ForbiddenBinSubdirectories.Contains(subDir.Name) && !subDir.Name.EndsWith(".Tests")) + { + foreach (FileInfo fileInfo in subDir.EnumerateFiles("*.xml", SearchOption.AllDirectories)) + { + yield return fileInfo; + } + } + } + + // 2) Find all the xml files in the top directory + foreach (FileInfo fileInfo in dirInfo.EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly)) + { + yield return fileInfo; + } + } + } + + private void LoadFile(FileInfo fileInfo, bool printSuccess) + { + if (!fileInfo.Exists) + { + Log.Error($"The IntelliSense xml file does not exist: {fileInfo.FullName}"); + return; + } + + xDoc = XDocument.Load(fileInfo.FullName); + + if (TryGetAssemblyName(xDoc, fileInfo.FullName, out string? assembly)) + { + return; + } + + int totalAdded = 0; + if (XmlHelper.TryGetChildElement(xDoc.Root!, "members", out XElement? xeMembers) && xeMembers != null) + { + foreach (XElement xeMember in xeMembers.Elements("member")) + { + IntelliSenseXmlMember member = new IntelliSenseXmlMember(xeMember, assembly); + + if (Config.IncludedAssemblies.Any(included => member.Assembly.StartsWith(included)) && + !Config.ExcludedAssemblies.Any(excluded => member.Assembly.StartsWith(excluded))) + { + // No namespaces provided by the user means they want to port everything from that assembly + if (!Config.IncludedNamespaces.Any() || + (Config.IncludedNamespaces.Any(included => member.Namespace.StartsWith(included)) && + !Config.ExcludedNamespaces.Any(excluded => member.Namespace.StartsWith(excluded)))) + { + totalAdded++; + Members.Add(member); + } + } + } + } + + if (printSuccess && totalAdded > 0) + { + Log.Success($"{totalAdded} IntelliSense xml member(s) added from xml file '{fileInfo.FullName}'"); + } + } + + // Verifies the file is properly formed while attempting to retrieve the assembly name. + private bool TryGetAssemblyName(XDocument? xDoc, string fileName, [NotNullWhen(returnValue: false)] out string? assembly) + { + assembly = null; + + if (xDoc == null) + { + Log.Error($"The XDocument was null: {fileName}"); + return true; + } + if (xDoc.Root == null) + { + Log.Error($"The IntelliSense xml file does not contain a root element: {fileName}"); + return true; + } + + if (xDoc.Root.Name != "doc") + { + Log.Error($"The IntelliSense xml file does not contain a doc element: {fileName}"); + return true; + } + + if (!xDoc.Root.HasElements) + { + Log.Error($"The IntelliSense xml file doc element not have any children: {fileName}"); + return true; + } + + if (xDoc.Root.Elements("assembly").Count() != 1) + { + Log.Error($"The IntelliSense xml file does not contain exactly 1 'assembly' element: {fileName}"); + return true; + } + + if (xDoc.Root.Elements("members").Count() != 1) + { + Log.Error($"The IntelliSense xml file does not contain exactly 1 'members' element: {fileName}"); + return true; + } + + XElement? xAssembly = xDoc.Root.Element("assembly"); + if (xAssembly == null) + { + Log.Error($"The assembly xElement is null: {fileName}"); + return true; + } + if (xAssembly.Elements("name").Count() != 1) + { + Log.Error($"The IntelliSense xml file assembly element does not contain exactly 1 'name' element: {fileName}"); + return true; + } + + assembly = xAssembly.Element("name")!.Value; + if (string.IsNullOrEmpty(assembly)) + { + Log.Error($"The IntelliSense xml file assembly string is null or empty: {fileName}"); + return true; + } + + return false; + } + } +} diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlException.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlException.cs new file mode 100644 index 0000000..44b3645 --- /dev/null +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlException.cs @@ -0,0 +1,49 @@ +using System.Xml.Linq; + +namespace Libraries.IntelliSenseXml +{ + internal class IntelliSenseXmlException + { + public XElement XEException + { + get; + private set; + } + + private string _cref = string.Empty; + public string Cref + { + get + { + if (string.IsNullOrWhiteSpace(_cref)) + { + _cref = XmlHelper.GetAttributeValue(XEException, "cref"); + } + return _cref; + } + } + + private string _value = string.Empty; + public string Value + { + get + { + if (string.IsNullOrWhiteSpace(_value)) + { + _value = XmlHelper.GetNodesInPlainText(XEException); + } + return _value; + } + } + + public IntelliSenseXmlException(XElement xeException) + { + XEException = xeException; + } + + public override string ToString() + { + return $"{Cref} - {Value}"; + } + } +} diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs new file mode 100644 index 0000000..c0c56e8 --- /dev/null +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Libraries.IntelliSenseXml +{ + internal class IntelliSenseXmlMember + { + private readonly XElement XEMember; + + public string Assembly { get; private set; } + + + private string _namespace = string.Empty; + public string Namespace + { + get + { + if (string.IsNullOrWhiteSpace(_namespace)) + { + string[] splittedParenthesis = Name.Split('(', StringSplitOptions.RemoveEmptyEntries); + string withoutParenthesisAndPrefix = splittedParenthesis[0][2..]; // Exclude the "X:" prefix + string[] splittedDots = withoutParenthesisAndPrefix.Split('.', StringSplitOptions.RemoveEmptyEntries); + + _namespace = string.Join('.', splittedDots.Take(splittedDots.Length - 1)); + } + + return _namespace; + } + } + + private string? _name; + /// + /// The API DocId. + /// + public string Name + { + get + { + if (_name == null) + { + _name = XmlHelper.GetAttributeValue(XEMember, "name"); + } + return _name; + } + } + + private List? _params; + public List Params + { + get + { + if (_params == null) + { + _params = XEMember.Elements("param").Select(x => new IntelliSenseXmlParam(x)).ToList(); + } + return _params; + } + } + + private List? _typeParams; + public List TypeParams + { + get + { + if (_typeParams == null) + { + _typeParams = XEMember.Elements("typeparam").Select(x => new IntelliSenseXmlTypeParam(x)).ToList(); + } + return _typeParams; + } + } + + private List? _exceptions; + public IEnumerable Exceptions + { + get + { + if (_exceptions == null) + { + _exceptions = XEMember.Elements("exception").Select(x => new IntelliSenseXmlException(x)).ToList(); + } + return _exceptions; + } + } + + private string? _summary; + public string Summary + { + get + { + if (_summary == null) + { + XElement xElement = XEMember.Element("summary"); + _summary = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; + } + return _summary; + } + } + + public string? _value; + public string Value + { + get + { + if (_value == null) + { + XElement xElement = XEMember.Element("value"); + _value = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; + } + return _value; + } + } + + private string? _returns; + public string Returns + { + get + { + if (_returns == null) + { + XElement xElement = XEMember.Element("returns"); + _returns = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; + } + return _returns; + } + } + + private string? _remarks; + public string Remarks + { + get + { + if (_remarks == null) + { + XElement xElement = XEMember.Element("remarks"); + _remarks = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; + } + return _remarks; + } + } + + public IntelliSenseXmlMember(XElement xeMember, string assembly) + { + if (string.IsNullOrEmpty(assembly)) + { + throw new ArgumentNullException(nameof(assembly)); + } + + XEMember = xeMember ?? throw new ArgumentNullException(nameof(xeMember)); + Assembly = assembly; + } + + public override string ToString() + { + return Name; + } + } +} diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlParam.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlParam.cs new file mode 100644 index 0000000..b5931a1 --- /dev/null +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlParam.cs @@ -0,0 +1,44 @@ +using System.Xml.Linq; + +namespace Libraries.IntelliSenseXml +{ + internal class IntelliSenseXmlParam + { + public XElement XEParam + { + get; + private set; + } + + private string _name = string.Empty; + public string Name + { + get + { + if (string.IsNullOrWhiteSpace(_name)) + { + _name = XmlHelper.GetAttributeValue(XEParam, "name"); + } + return _name; + } + } + + private string _value = string.Empty; + public string Value + { + get + { + if (string.IsNullOrWhiteSpace(_value)) + { + _value = XmlHelper.GetNodesInPlainText(XEParam); + } + return _value; + } + } + + public IntelliSenseXmlParam(XElement xeParam) + { + XEParam = xeParam; + } + } +} diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlTypeParam.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlTypeParam.cs new file mode 100644 index 0000000..7fda8f2 --- /dev/null +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlTypeParam.cs @@ -0,0 +1,40 @@ +using System.Xml.Linq; + +namespace Libraries.IntelliSenseXml +{ + internal class IntelliSenseXmlTypeParam + { + public XElement XETypeParam; + + private string _name = string.Empty; + public string Name + { + get + { + if (string.IsNullOrWhiteSpace(_name)) + { + _name = XmlHelper.GetAttributeValue(XETypeParam, "name"); + } + return _name; + } + } + + private string _value = string.Empty; + public string Value + { + get + { + if (string.IsNullOrWhiteSpace(_value)) + { + _value = XmlHelper.GetNodesInPlainText(XETypeParam); + } + return _value; + } + } + + public IntelliSenseXmlTypeParam(XElement xeTypeParam) + { + XETypeParam = xeTypeParam; + } + } +} \ No newline at end of file From 5027a9b6bdf87001d9b002ed6ae5158cc38df677 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:42:28 -0800 Subject: [PATCH 32/65] Triple Slash rewriter. --- .../TripleSlashSyntaxRewriter.cs | 409 ++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs new file mode 100644 index 0000000..b86008d --- /dev/null +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -0,0 +1,409 @@ +#nullable enable +using Libraries.Docs; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Libraries.RoslynTripleSlash +{ + internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter + { + private const string BoilerplateText = "Comments located in main file."; + + private DocsCommentsContainer DocsComments { get; } + private SemanticModel Model { get; } + private bool UseBoilerplate { get; } + + public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticModel model, Location location, SyntaxTree tree, bool useBoilerplate) : base(visitIntoStructuredTrivia: true) + { + DocsComments = docsComments; + Model = model; + UseBoilerplate = useBoilerplate; + } + + public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) + { + SyntaxNode? baseNode = base.VisitClassDeclaration(node); + + ISymbol? symbol = Model.GetDeclaredSymbol(node); + if (symbol == null) + { + Log.Warning($"Symbol is null."); + return baseNode; + } + + return VisitType(baseNode, symbol); + } + + public override SyntaxNode? VisitConstructorDeclaration(ConstructorDeclarationSyntax node) => + VisitBaseMethodDeclaration(node); + + public override SyntaxNode? VisitEnumDeclaration(EnumDeclarationSyntax node) => + VisitMemberDeclaration(node); + + public override SyntaxNode? VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) => + VisitMemberDeclaration(node); + + public override SyntaxNode? VisitEventDeclaration(EventDeclarationSyntax node) => + VisitMemberDeclaration(node); + + public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node) => + VisitMemberDeclaration(node); + + public override SyntaxNode? VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) + { + SyntaxNode? baseNode = base.VisitInterfaceDeclaration(node); + + ISymbol? symbol = Model.GetDeclaredSymbol(node); + if (symbol == null) + { + Log.Warning($"Symbol is null."); + return baseNode; + } + + return VisitType(baseNode, symbol); + } + + public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) => + VisitBaseMethodDeclaration(node); + + public override SyntaxNode? VisitPropertyDeclaration(PropertyDeclarationSyntax node) + { + if (!TryGetMember(node, out DocsMember? member)) + { + return node; + } + + string summaryText = BoilerplateText; + string valueText = BoilerplateText; + + if (!UseBoilerplate) + { + summaryText = member.Summary; + valueText = member.Value; + } + + SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); + + SyntaxTriviaList summary = GetSummary(summaryText, leadingWhitespace); + SyntaxTriviaList value = GetValue(valueText, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); + + return GetNodeWithTrivia(node, summary, value, remarks, exceptions, seealsos); + } + + public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node) + { + SyntaxNode? baseNode = base.VisitStructDeclaration(node); + + ISymbol? symbol = Model.GetDeclaredSymbol(node); + if (symbol == null) + { + Log.Warning($"Symbol is null."); + return baseNode; + } + + return VisitType(baseNode, symbol); + } + + private SyntaxNode? VisitType(SyntaxNode? node, 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; + } + + string summaryText = BoilerplateText; + string remarksText = string.Empty; + + if (!UseBoilerplate) + { + if (!TryGetType(node, symbol, out DocsType? type)) + { + return node; + } + + summaryText = type.Summary; + remarksText = type.Remarks; + } + + SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); + + SyntaxTriviaList summary = GetSummary(summaryText, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(remarksText, leadingWhitespace); + + return GetNodeWithTrivia(node, summary, remarks); + } + + private SyntaxNode GetNodeWithTrivia(SyntaxNode node, params SyntaxTriviaList[] trivias) + { + SyntaxTriviaList finalTrivia = new(SyntaxFactory.CarriageReturnLineFeed); // Space to separate from previous definition + foreach (SyntaxTriviaList t in trivias) + { + finalTrivia = finalTrivia.AddRange(t); + } + finalTrivia = finalTrivia.AddRange(GetLeadingWhitespace(node)); // spaces before type declaration + + return node.WithLeadingTrivia(finalTrivia); + } + + private SyntaxNode? VisitBaseMethodDeclaration(BaseMethodDeclarationSyntax node) + { + if (!TryGetMember(node, out DocsMember? member)) + { + return node; + } + + SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); + + SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); + + SyntaxTriviaList parameters = new(); + foreach (SyntaxTriviaList parameterTrivia in member.Params.Select( + param => GetParam(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) + { + parameters = parameters.AddRange(parameterTrivia); + } + + SyntaxTriviaList typeParameters = new(); + foreach (SyntaxTriviaList typeParameterTrivia in member.TypeParams.Select( + param => GetTypeParam(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) + { + typeParameters = typeParameters.AddRange(typeParameterTrivia); + } + + SyntaxTriviaList returns = GetReturns(UseBoilerplate ? BoilerplateText : member.Returns, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); + + return GetNodeWithTrivia(node, summary, parameters, typeParameters, returns, remarks, exceptions, seealsos); + } + + private SyntaxNode? VisitMemberDeclaration(MemberDeclarationSyntax node) + { + if (!TryGetMember(node, out DocsMember? member)) + { + return node; + } + + SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); + + SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + + SyntaxTriviaList exceptions = new(); + // No need to add exceptions in secondary files + if (!UseBoilerplate && member.Exceptions.Any()) + { + foreach (SyntaxTriviaList exceptionsTrivia in member.Exceptions.Select( + exception => GetException(exception.Cref, exception.Value, leadingWhitespace))) + { + exceptions = exceptions.AddRange(exceptionsTrivia); + } + } + + return GetNodeWithTrivia(node, summary, remarks, exceptions); + } + + private SyntaxTriviaList GetLeadingWhitespace(SyntaxNode node) => + node.GetLeadingTrivia().Where(t => t.IsKind(SyntaxKind.WhitespaceTrivia)).ToSyntaxTriviaList(); + + private SyntaxTriviaList GetSummary(string text, SyntaxTriviaList leadingWhitespace) + { + SyntaxList contents = GetContentsInRows(text); + XmlElementSyntax element = SyntaxFactory.XmlSummaryElement(contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitespace) + { + if (!UseBoilerplate && !text.IsDocsEmpty()) + { + string trimmedRemarks = text.RemoveSubstrings("").Trim(); + SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed)); + XmlNodeSyntax xmlRemarksContent = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); + XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(xmlRemarksContent); + + return GetXmlTrivia(xmlRemarks, leadingWhitespace); + + //DocumentationCommentTriviaSyntax triviaNode = SyntaxFactory.DocumentationComment(SyntaxKind.SingleLineDocumentationCommentTrivia, content); + //SyntaxTrivia docCommentTrivia = SyntaxFactory.Trivia(triviaNode); + + //return leadingWhitespace + //.Add(docCommentTrivia) + //.Add(SyntaxFactory.CarriageReturnLineFeed); + } + + return new(); + } + + private SyntaxTriviaList GetValue(string text, SyntaxTriviaList leadingWhitespace) + { + SyntaxList contents = GetContentsInRows(text); + XmlElementSyntax element = SyntaxFactory.XmlValueElement(contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + private SyntaxTriviaList GetParam(string name, string text, SyntaxTriviaList leadingWhitespace) + { + SyntaxList contents = GetContentsInRows(text); + XmlElementSyntax element = SyntaxFactory.XmlParamElement(name, contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + private SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace) + { + var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute("name", text)); + SyntaxList contents = GetContentsInRows(text); + return GetXmlTrivia("typeparam", attribute, contents, leadingWhitespace); + } + + private SyntaxTriviaList GetReturns(string text, SyntaxTriviaList leadingWhitespace) + { + // For when returns is empty because the method returns void + if (string.IsNullOrWhiteSpace(text)) + { + return new(); + } + + SyntaxList contents = GetContentsInRows(text); + XmlElementSyntax element = SyntaxFactory.XmlReturnsElement(contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + private SyntaxTriviaList GetExceptions(DocsMember member, SyntaxTriviaList leadingWhitespace) + { + SyntaxTriviaList exceptions = new(); + // No need to add exceptions in secondary files + if (!UseBoilerplate && member.Exceptions.Any()) + { + foreach (SyntaxTriviaList exceptionsTrivia in member.Exceptions.Select( + exception => GetException(exception.Cref, exception.Value, leadingWhitespace))) + { + exceptions = exceptions.AddRange(exceptionsTrivia); + } + } + return exceptions; + } + + private SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace) + { + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); + XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text, leadingWhitespace)); + XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + private SyntaxTriviaList GetSeeAlsos(DocsMember member, SyntaxTriviaList leadingWhitespace) + { + SyntaxTriviaList seealsos = new(); + // No need to add exceptions in secondary files + if (!UseBoilerplate && member.SeeAlsos.Any()) + { + foreach (SyntaxTriviaList seealsoTrivia in member.SeeAlsos.Select( + s => GetSeeAlso(s.Cref, leadingWhitespace))) + { + seealsos = seealsos.AddRange(seealsoTrivia); + } + } + return seealsos; + } + + private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace) + { + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); + XmlEmptyElementSyntax element = SyntaxFactory.XmlSeeAlsoElement(crefSyntax); + return GetXmlTrivia(element, leadingWhitespace); + } + + private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace) + { + string whitespace = leadingWhitespace.ToFullString().Replace(Environment.NewLine, ""); + var tokens = new List(); + foreach (string line in text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)) + { + tokens.Add(SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace)); // Needs to be textnewline, and below needs to be textliteral + tokens.Add(SyntaxFactory.XmlTextLiteral(SyntaxTriviaList.Create(SyntaxFactory.SyntaxTrivia(SyntaxKind.DocumentationCommentExteriorTrivia, string.Empty)), line, line, default)); + } + + return SyntaxFactory.TokenList(tokens); + } + + private SyntaxList GetContentsInRows(string text) + { + return new(SyntaxFactory.XmlText(text)); // TODO: Press enter! + } + + private SyntaxTriviaList GetXmlTrivia(XmlNodeSyntax node, SyntaxTriviaList leadingWhitespace) + { + DocumentationCommentTriviaSyntax docComment = SyntaxFactory.DocumentationComment(node); + SyntaxTrivia docCommentTrivia = SyntaxFactory.Trivia(docComment); + + return leadingWhitespace + .Add(docCommentTrivia) + .Add(SyntaxFactory.CarriageReturnLineFeed); + } + + // Generates a custom SyntaxTrivia object containing a triple slashed xml element with optional attributes. + // Looks like below (excluding square brackets): + // [ /// text] + private SyntaxTriviaList GetXmlTrivia(string name, SyntaxList attributes, SyntaxList 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, contents, end); + + return GetXmlTrivia(element, leadingWhitespace); + } + + private bool TryGetMember(SyntaxNode node, [NotNullWhen(returnValue: true)] out DocsMember? member) + { + member = null; + if (Model.GetDeclaredSymbol(node) is ISymbol symbol) + { + string? docId = symbol.GetDocumentationCommentId(); + if (!string.IsNullOrWhiteSpace(docId)) + { + member = DocsComments.Members.FirstOrDefault(m => m.DocId == docId); + } + } + return member != null; + } + + private bool TryGetType(SyntaxNode node, ISymbol symbol, [NotNullWhen(returnValue: true)] out DocsType? type) + { + type = null; + + string? docId = symbol.GetDocumentationCommentId(); + if (!string.IsNullOrWhiteSpace(docId)) + { + type = DocsComments.Types.FirstOrDefault(t => t.DocId == docId); + } + + return type != null; + } + } +} From 8a3d586f5f1a19875f4a349708602bf3a28bdd9f Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:43:20 -0800 Subject: [PATCH 33/65] Shared code changes. --- Libraries/Configuration.cs | 660 +++++++++++++++++++++++ Libraries/Extensions.cs | 42 ++ Libraries/Libraries.csproj | 26 + Libraries/Log.cs | 408 ++++++++++++++ Libraries/ToDocsPorter.cs | 896 +++++++++++++++++++++++++++++++ Libraries/ToTripleSlashPorter.cs | 133 +++++ Libraries/XmlHelper.cs | 320 +++++++++++ 7 files changed, 2485 insertions(+) create mode 100644 Libraries/Configuration.cs create mode 100644 Libraries/Extensions.cs create mode 100644 Libraries/Libraries.csproj create mode 100644 Libraries/Log.cs create mode 100644 Libraries/ToDocsPorter.cs create mode 100644 Libraries/ToTripleSlashPorter.cs create mode 100644 Libraries/XmlHelper.cs diff --git a/Libraries/Configuration.cs b/Libraries/Configuration.cs new file mode 100644 index 0000000..b39af89 --- /dev/null +++ b/Libraries/Configuration.cs @@ -0,0 +1,660 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; + +namespace Libraries +{ + public class Configuration + { + private static readonly char Separator = ','; + + public enum PortingDirection + { + ToDocs, + ToTripleSlash + } + + private enum Mode + { + BinLog, + CsProj, + DisablePrompts, + Direction, + Docs, + ExceptionCollisionThreshold, + ExcludedAssemblies, + ExcludedNamespaces, + ExcludedTypes, + IncludedAssemblies, + IncludedNamespaces, + IncludedTypes, + Initial, + IntelliSense, + PortExceptionsExisting, + PortExceptionsNew, + PortMemberParams, + PortMemberProperties, + PortMemberReturns, + PortMemberRemarks, + PortMemberSummaries, + PortMemberTypeParams, + PortTypeParams, // Params of a Type + PortTypeRemarks, + PortTypeSummaries, + PortTypeTypeParams, // TypeParams of a Type + PrintUndoc, + Save, + SkipInterfaceImplementations, + SkipInterfaceRemarks + } + + // The default boilerplate string for what dotnet-api-docs + // considers an empty (undocumented) API element. + public static readonly string ToBeAdded = "To be added."; + + public static readonly string[] ForbiddenBinSubdirectories = new[] { "binplacePackages", "docs", "mscorlib", "native", "netfx", "netstandard", "pkg", "Product", "ref", "runtime", "shimsTargetRuntime", "testhost", "tests", "winrt" }; + + public readonly string BinLogPath = "output.binlog"; + public bool BinLogger { get; private set; } = false; + public FileInfo? CsProj { get; private set; } + public PortingDirection Direction { get; private set; } = PortingDirection.ToDocs; + public List DirsIntelliSense { get; } = new List(); + public List DirsDocsXml { get; } = new List(); + public bool DisablePrompts { get; set; } = false; + public int ExceptionCollisionThreshold { get; set; } = 70; + public HashSet ExcludedAssemblies { get; } = new HashSet(); + public HashSet ExcludedNamespaces { get; } = new HashSet(); + public HashSet ExcludedTypes { get; } = new HashSet(); + public HashSet IncludedAssemblies { get; } = new HashSet(); + public HashSet IncludedNamespaces { get; } = new HashSet(); + public HashSet IncludedTypes { get; } = new HashSet(); + public bool PortExceptionsExisting { get; set; } = false; + public bool PortExceptionsNew { get; set; } = true; + public bool PortMemberParams { get; set; } = true; + public bool PortMemberProperties { get; set; } = true; + public bool PortMemberReturns { get; set; } = true; + public bool PortMemberRemarks { get; set; } = true; + public bool PortMemberSummaries { get; set; } = true; + public bool PortMemberTypeParams { get; set; } = true; + /// + /// Params of a Type. + /// + public bool PortTypeParams { get; set; } = true; + public bool PortTypeRemarks { get; set; } = true; + public bool PortTypeSummaries { get; set; } = true; + /// + /// TypeParams of a Type. + /// + public bool PortTypeTypeParams { get; set; } = true; + public bool PrintUndoc { get; set; } = false; + public bool Save { get; set; } = false; + public bool SkipInterfaceImplementations { get; set; } = false; + public bool SkipInterfaceRemarks { get; set; } = true; + + public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) + { + Mode mode = Mode.Initial; + + Log.Info("Verifying CLI arguments..."); + + if (args == null || args.Length == 0) + { + Log.ErrorPrintHelpAndExit("No arguments passed to the executable."); + } + + Configuration config = new Configuration(); + + foreach (string arg in args!) + { + switch (mode) + { + case Mode.BinLog: + { + config.BinLogger = ParseOrExit(arg, "Create a binlog"); + mode = Mode.Initial; + break; + } + + case Mode.CsProj: + { + if (string.IsNullOrWhiteSpace(arg)) + { + Log.ErrorAndExit("You must specify a *.csproj path."); + } + else if (!File.Exists(arg)) + { + Log.ErrorAndExit($"The *.csproj file does not exist: {arg}"); + } + else + { + string ext = Path.GetExtension(arg).ToUpperInvariant(); + if (ext != ".CSPROJ") + { + Log.ErrorAndExit($"The file does not have a *.csproj extension: {arg}"); + } + } + config.CsProj = new FileInfo(arg); + mode = Mode.Initial; + break; + } + + case Mode.DisablePrompts: + { + config.DisablePrompts = ParseOrExit(arg, "Disable prompts"); + mode = Mode.Initial; + break; + } + + case Mode.Direction: + { + switch (arg.ToUpperInvariant()) + { + case "TODOCS": + config.Direction = PortingDirection.ToDocs; + break; + case "TOTRIPLESLASH": + config.Direction = PortingDirection.ToTripleSlash; + break; + default: + Log.ErrorAndExit($"Unrecognized direction value: {arg}"); + break; + } + mode = Mode.Initial; + break; + } + + case Mode.Docs: + { + string[] splittedDirPaths = arg.Split(',', StringSplitOptions.RemoveEmptyEntries); + + Log.Cyan($"Specified Docs xml locations:"); + foreach (string dirPath in splittedDirPaths) + { + DirectoryInfo dirInfo = new DirectoryInfo(dirPath); + if (!dirInfo.Exists) + { + Log.ErrorAndExit($"This Docs xml directory does not exist: {dirPath}"); + } + + config.DirsDocsXml.Add(dirInfo); + Log.Info($" - {dirPath}"); + } + + mode = Mode.Initial; + break; + } + + case Mode.ExceptionCollisionThreshold: + { + if (!int.TryParse(arg, out int value)) + { + Log.ErrorAndExit($"Invalid int value for 'Exception collision threshold' argument: {arg}"); + } + else if (value < 1 || value > 100) + { + Log.ErrorAndExit($"Value needs to be between 0 and 100: {value}"); + } + + config.ExceptionCollisionThreshold = value; + + Log.Cyan($"Exception collision threshold:"); + Log.Info($" - {value}"); + break; + } + + case Mode.ExcludedAssemblies: + { + string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + + if (splittedArg.Length > 0) + { + Log.Cyan("Excluded assemblies:"); + foreach (string assembly in splittedArg) + { + Log.Cyan($" - {assembly}"); + config.ExcludedAssemblies.Add(assembly); + } + } + else + { + Log.ErrorPrintHelpAndExit("You must specify at least one assembly."); + } + + mode = Mode.Initial; + break; + } + + case Mode.ExcludedNamespaces: + { + string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + + if (splittedArg.Length > 0) + { + Log.Cyan("Excluded namespaces:"); + foreach (string ns in splittedArg) + { + Log.Cyan($" - {ns}"); + config.ExcludedNamespaces.Add(ns); + } + } + else + { + Log.ErrorPrintHelpAndExit("You must specify at least one namespace."); + } + + mode = Mode.Initial; + break; + } + + case Mode.ExcludedTypes: + { + string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + + if (splittedArg.Length > 0) + { + Log.Cyan($"Excluded types:"); + foreach (string typeName in splittedArg) + { + Log.Cyan($" - {typeName}"); + config.ExcludedTypes.Add(typeName); + } + } + else + { + Log.ErrorPrintHelpAndExit("You must specify at least one type name."); + } + + mode = Mode.Initial; + break; + } + + case Mode.IncludedAssemblies: + { + string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + + if (splittedArg.Length > 0) + { + Log.Cyan($"Included assemblies:"); + foreach (string assembly in splittedArg) + { + Log.Cyan($" - {assembly}"); + config.IncludedAssemblies.Add(assembly); + } + } + else + { + Log.ErrorPrintHelpAndExit("You must specify at least one assembly."); + } + + mode = Mode.Initial; + break; + } + + case Mode.IncludedNamespaces: + { + string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + + if (splittedArg.Length > 0) + { + Log.Cyan($"Included namespaces:"); + foreach (string ns in splittedArg) + { + Log.Cyan($" - {ns}"); + config.IncludedNamespaces.Add(ns); + } + } + else + { + Log.ErrorPrintHelpAndExit("You must specify at least one namespace."); + } + + mode = Mode.Initial; + break; + } + + case Mode.IncludedTypes: + { + string[] splittedArg = arg.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + + if (splittedArg.Length > 0) + { + Log.Cyan($"Included types:"); + foreach (string typeName in splittedArg) + { + Log.Cyan($" - {typeName}"); + config.IncludedTypes.Add(typeName); + } + } + else + { + Log.ErrorPrintHelpAndExit("You must specify at least one type name."); + } + + mode = Mode.Initial; + break; + } + + case Mode.Initial: + { + switch (arg.ToUpperInvariant()) + { + case "-BINLOG": + mode = Mode.BinLog; + break; + + case "-CSPROJ": + mode = Mode.CsProj; + break; + + case "-DIRECTION": + mode = Mode.Direction; + break; + + case "-DOCS": + mode = Mode.Docs; + break; + + case "-DISABLEPROMPTS": + mode = Mode.DisablePrompts; + break; + + case "EXCEPTIONCOLLISIONTHRESHOLD": + mode = Mode.ExceptionCollisionThreshold; + break; + + case "-EXCLUDEDASSEMBLIES": + mode = Mode.ExcludedAssemblies; + break; + + case "-EXCLUDEDNAMESPACES": + mode = Mode.ExcludedNamespaces; + break; + + case "-EXCLUDEDTYPES": + mode = Mode.ExcludedTypes; + break; + + case "-H": + case "-HELP": + Log.PrintHelp(); + Environment.Exit(0); + break; + + case "-INCLUDEDASSEMBLIES": + mode = Mode.IncludedAssemblies; + break; + + case "-INCLUDEDNAMESPACES": + mode = Mode.IncludedNamespaces; + break; + + case "-INCLUDEDTYPES": + mode = Mode.IncludedTypes; + break; + + case "-INTELLISENSE": + mode = Mode.IntelliSense; + break; + + case "-PORTEXCEPTIONSEXISTING": + mode = Mode.PortExceptionsExisting; + break; + + case "-PORTEXCEPTIONSNEW": + mode = Mode.PortExceptionsNew; + break; + + case "-PORTMEMBERPARAMS": + mode = Mode.PortMemberParams; + break; + + case "-PORTMEMBERPROPERTIES": + mode = Mode.PortMemberProperties; + break; + + case "-PORTMEMBERRETURNS": + mode = Mode.PortMemberReturns; + break; + + case "-PORTMEMBERREMARKS": + mode = Mode.PortMemberRemarks; + break; + + case "-PORTMEMBERSUMMARIES": + mode = Mode.PortMemberSummaries; + break; + + case "-PORTMEMBERTYPEPARAMS": + mode = Mode.PortMemberTypeParams; + break; + + case "-PORTTYPEPARAMS": // Params of a Type + mode = Mode.PortTypeParams; + break; + + case "-PORTTYPEREMARKS": + mode = Mode.PortTypeRemarks; + break; + + case "-PORTTYPESUMMARIES": + mode = Mode.PortTypeSummaries; + break; + + case "-PORTTYPETYPEPARAMS": // TypeParams of a Type + mode = Mode.PortTypeTypeParams; + break; + + case "-PRINTUNDOC": + mode = Mode.PrintUndoc; + break; + + case "-SAVE": + mode = Mode.Save; + break; + + case "-SKIPINTERFACEIMPLEMENTATIONS": + mode = Mode.SkipInterfaceImplementations; + break; + + case "-SKIPINTERFACEREMARKS": + mode = Mode.SkipInterfaceRemarks; + break; + + default: + Log.ErrorPrintHelpAndExit($"Unrecognized argument: {arg}"); + break; + } + break; + } + + case Mode.IntelliSense: + { + string[] splittedDirPaths = arg.Split(',', StringSplitOptions.RemoveEmptyEntries); + + Log.Cyan($"Specified IntelliSense locations:"); + foreach (string dirPath in splittedDirPaths) + { + DirectoryInfo dirInfo = new DirectoryInfo(dirPath); + if (!dirInfo.Exists) + { + Log.ErrorAndExit($"This IntelliSense directory does not exist: {dirPath}"); + } + + config.DirsIntelliSense.Add(dirInfo); + Log.Info($" - {dirPath}"); + } + + mode = Mode.Initial; + break; + } + + case Mode.PortExceptionsExisting: + { + config.PortExceptionsExisting = ParseOrExit(arg, "Port existing exceptions"); + mode = Mode.Initial; + break; + } + + case Mode.PortExceptionsNew: + { + config.PortExceptionsNew = ParseOrExit(arg, "Port new exceptions"); + mode = Mode.Initial; + break; + } + + case Mode.PortMemberParams: + { + config.PortMemberParams = ParseOrExit(arg, "Port member Params"); + mode = Mode.Initial; + break; + } + + case Mode.PortMemberProperties: + { + config.PortMemberProperties = ParseOrExit(arg, "Port member Properties"); + mode = Mode.Initial; + break; + } + + case Mode.PortMemberRemarks: + { + config.PortMemberRemarks = ParseOrExit(arg, "Port member Remarks"); + mode = Mode.Initial; + break; + } + + case Mode.PortMemberReturns: + { + config.PortMemberReturns = ParseOrExit(arg, "Port member Returns"); + mode = Mode.Initial; + break; + } + + case Mode.PortMemberSummaries: + { + config.PortMemberSummaries = ParseOrExit(arg, "Port member Summaries"); + mode = Mode.Initial; + break; + } + + case Mode.PortMemberTypeParams: + { + config.PortMemberTypeParams = ParseOrExit(arg, "Port member TypeParams"); + mode = Mode.Initial; + break; + } + + case Mode.PortTypeParams: // Params of a Type + { + config.PortTypeParams = ParseOrExit(arg, "Port Type Params"); + mode = Mode.Initial; + break; + } + + case Mode.PortTypeRemarks: + { + config.PortTypeRemarks = ParseOrExit(arg, "Port Type Remarks"); + mode = Mode.Initial; + break; + } + + case Mode.PortTypeSummaries: + { + config.PortTypeSummaries = ParseOrExit(arg, "Port Type Summaries"); + mode = Mode.Initial; + break; + } + + case Mode.PortTypeTypeParams: // TypeParams of a Type + { + config.PortTypeTypeParams = ParseOrExit(arg, "Port Type TypeParams"); + mode = Mode.Initial; + break; + } + + case Mode.PrintUndoc: + { + config.PrintUndoc = ParseOrExit(arg, "Print undoc"); + mode = Mode.Initial; + break; + } + + case Mode.Save: + { + config.Save = ParseOrExit(arg, "Save"); + mode = Mode.Initial; + break; + } + + case Mode.SkipInterfaceImplementations: + { + config.SkipInterfaceImplementations = ParseOrExit(arg, "Skip interface implementations"); + mode = Mode.Initial; + break; + } + + case Mode.SkipInterfaceRemarks: + { + config.SkipInterfaceRemarks = ParseOrExit(arg, "Skip appending interface remarks"); + mode = Mode.Initial; + break; + } + + default: + { + Log.ErrorPrintHelpAndExit("Unexpected mode."); + break; + } + } + } + + if (mode != Mode.Initial) + { + Log.ErrorPrintHelpAndExit("You missed an argument value."); + } + + if (config.DirsDocsXml == null) + { + Log.ErrorPrintHelpAndExit($"You must specify a path to the dotnet-api-docs xml folder using '-{nameof(Mode.Docs)}'."); + } + + if (config.Direction == PortingDirection.ToDocs) + { + if (config.DirsIntelliSense.Count == 0) + { + Log.ErrorPrintHelpAndExit($"You must specify at least one IntelliSense & DLL folder using '-{nameof(Mode.IntelliSense)}'."); + } + } + + if (config.Direction == PortingDirection.ToTripleSlash) + { + if (config.CsProj == null) + { + Log.ErrorPrintHelpAndExit($"You must specify a *.csproj file using '-{nameof(Mode.CsProj)}'."); + } + } + + if (config.IncludedAssemblies.Count == 0) + { + Log.ErrorPrintHelpAndExit($"You must specify at least one assembly with {nameof(IncludedAssemblies)}."); + } + + return config; + } + + // Tries to parse the user argument string as boolean, and if it fails, exits the program. + private static bool ParseOrExit(string arg, string paramFriendlyName) + { + if (!bool.TryParse(arg, out bool value)) + { + Log.ErrorAndExit($"Invalid boolean value for '{paramFriendlyName}' argument: {arg}"); + } + + Log.Cyan($"{paramFriendlyName}:"); + Log.Info($" - {value}"); + + return value; + } + } +} diff --git a/Libraries/Extensions.cs b/Libraries/Extensions.cs new file mode 100644 index 0000000..8f92ab7 --- /dev/null +++ b/Libraries/Extensions.cs @@ -0,0 +1,42 @@ +#nullable enable +using System.Collections.Generic; + +namespace Libraries +{ + // Provides generic extension methods. + internal static class Extensions + { + // Adds a string to a list of strings if the element is not there yet. The method makes sure to escape unexpected curly brackets to prevent formatting exceptions. + public static void AddIfNotExists(this List list, string element) + { + string cleanedElement = element.Escaped(); + if (!list.Contains(cleanedElement)) + { + list.Add(cleanedElement); + } + } + + // Removes the specified subtrings from another string + public static string RemoveSubstrings(this string oldString, params string[] stringsToRemove) + { + string newString = oldString; + foreach (string toRemove in stringsToRemove) + { + if (newString.Contains(toRemove)) + { + newString = newString.Replace(toRemove, string.Empty); + } + } + return newString; + } + + // Some API DocIDs with types contain "{" and "}" to enclose the typeparam, which causes + // an exception to be thrown when trying to embed the string in a formatted string. + public static string Escaped(this string str) => str.Replace("{", "{{").Replace("}", "}}"); + + // Checks if the passed string is considered "empty" according to the Docs repo rules. + public static bool IsDocsEmpty(this string? s) => + string.IsNullOrWhiteSpace(s) || s == Configuration.ToBeAdded; + } + +} diff --git a/Libraries/Libraries.csproj b/Libraries/Libraries.csproj new file mode 100644 index 0000000..2ad72f9 --- /dev/null +++ b/Libraries/Libraries.csproj @@ -0,0 +1,26 @@ + + + + Library + net5.0 + Microsoft + carlossanlop + enable + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Libraries/Log.cs b/Libraries/Log.cs new file mode 100644 index 0000000..5866fe6 --- /dev/null +++ b/Libraries/Log.cs @@ -0,0 +1,408 @@ +#nullable enable +using System; + +namespace Libraries +{ + internal class Log + { + private static void WriteLine(string format, params object[]? args) + { + if (args == null || args.Length == 0) + { + Console.WriteLine(format); + } + else + { + Console.WriteLine(format, args); + } + } + + private static void Write(string format, params object[]? args) + { + if (args == null || args.Length == 0) + { + Console.Write(format); + } + else + { + Console.Write(format, args); + } + } + + public static void Print(bool endline, ConsoleColor foregroundColor, string format, params object[]? args) + { + ConsoleColor initialColor = Console.ForegroundColor; + Console.ForegroundColor = foregroundColor; + if (endline) + { + WriteLine(format, args); + } + else + { + Write(format, args); + } + Console.ForegroundColor = initialColor; + } + + public static void Info(string format) + { + Info(format, null); + } + + public static void Info(string format, params object[]? args) + { + Info(true, format, args); + } + + public static void Info(bool endline, string format, params object[]? args) + { + Print(endline, ConsoleColor.White, format, args); + } + + public static void Success(string format) + { + Success(format, null); + } + + public static void Success(string format, params object[]? args) + { + Success(true, format, args); + } + + public static void Success(bool endline, string format, params object[]? args) + { + Print(endline, ConsoleColor.Green, format, args); + } + + public static void Warning(string format) + { + Warning(format, null); + } + + public static void Warning(string format, params object[]? args) + { + Warning(true, format, args); + } + + public static void Warning(bool endline, string format, params object[]? args) + { + Print(endline, ConsoleColor.Yellow, format, args); + } + + public static void Error(string format) + { + Error(format, null); + } + + public static void Error(string format, params object[]? args) + { + Error(true, format, args); + } + + public static void Error(bool endline, string format, params object[]? args) + { + Print(endline, ConsoleColor.Red, format, args); + } + + public static void Cyan(string format) + { + Cyan(format, null); + } + + public static void Cyan(string format, params object[]? args) + { + Cyan(true, format, args); + } + + public static void Magenta(bool endline, string format, params object[]? args) + { + Print(endline, ConsoleColor.Magenta, format, args); + } + + public static void Magenta(string format) + { + Magenta(format, null); + } + + public static void Magenta(string format, params object[]? args) + { + Magenta(true, format, args); + } + + public static void Cyan(bool endline, string format, params object[]? args) + { + Print(endline, ConsoleColor.Cyan, format, args); + } + + public static void Assert(bool condition, string format, params object[]? args) + { + Assert(true, condition, format, args); + } + + public static void Assert(bool endline, bool condition, string format, params object[]? args) + { + if (condition) + { + Success(endline, format, args); + } + else + { + Error(endline, format, args); + } + } + + public static void Line() + { + Console.WriteLine(); + } + + public delegate void PrintHelpFunction(); + + public static void ErrorAndExit(string format, params object[]? args) + { + Error(format, args); + Environment.Exit(0); + } + + public static void ErrorPrintHelpAndExit(string format, params object[]? args) + { + PrintHelp(); + Error(format, args); + Environment.Exit(0); + } + + public static void PrintHelp() + { + Cyan(@" +This tool finds and ports triple slash comments found in .NET repos but do not yet exist in the dotnet-api-docs repo. + +The instructions below assume %SourceRepos% is the root folder of all your git cloned projects. + +Options: + + MANDATORY + ------------------------------------------------------------ + | PARAMETER | TYPE | DESCRIPTION | + ------------------------------------------------------------ + + -Docs comma-separated A comma separated list (no spaces) of absolute directory paths where the Docs xml files are located. + The xml files will be searched for recursively. + If any of the segments in the path may contain spaces, make sure to enclose the path in double quotes. + folder paths Known locations: + > Runtime: %SourceRepos%\dotnet-api-docs\xml + > WPF: %SourceRepos%\dotnet-api-docs\xml + > WinForms: %SourceRepos%\dotnet-api-docs\xml + > ASP.NET MVC: %SourceRepos%\AspNetApiDocs\aspnet-mvc\xml + > ASP.NET Core: %SourceRepos%\AspNetApiDocs\aspnet-core\xml + Usage example: + -Docs ""%SourceRepos%\dotnet-api-docs\xml\System.IO.FileSystem\"",%SourceRepos%\AspNetApiDocs\aspnet-mvc\xml + + -IntelliSense comma-separated Mandatory only when using '-Direction ToDocs' to port from IntelliSense xml to Docs. + folder paths A comma separated list (no spaces) of absolute directory paths where we the IntelliSense xml files + are located. Usually it's the 'artifacts/bin' folder in your source code repo. + The IntelliSense xml files will be searched for recursively. You must specify the root folder (usually 'bin'), + which contains all the subfolders whose names are assemblies or namespaces. Only those names specified + with '-IncludedAssemblies' and '-IncludedNamespaces' will be recursed. + If any of the segments in the path may contain spaces, make sure to enclose the path in double quotes. + Known locations: + > Runtime: %SourceRepos%\runtime\artifacts\bin\ + > CoreCLR: %SourceRepos%\runtime\artifacts\bin\coreclr\Windows_NT.x64.Release\IL\ + > WinForms: %SourceRepos%\winforms\artifacts\bin\ + > WPF: %SourceRepos%\wpf\artifacts\bin\ + Usage example: + -IntelliSense ""%SourceRepos%\corefx\artifacts\bin\"",%SourceRepos%\winforms\artifacts\bin\ + + -IncludedAssemblies string list Comma separated list (no spaces) of assemblies to include. + This argument prevents loading everything in the specified folder. + Usage example: + -IncludedAssemblies System.IO,System.Runtime + + IMPORTANT: + Namespaces usually match the assembly name. There are some exceptions, like with types that live in + the System.Runtime assembly. For those cases, make sure to also specify the -IncludedNamespaces argument. + + -CsProj file path Mandatory only when using '-Direction ToTripleSlash' to port from Docs to triple slash comments in source. + An absolute path to a *.csproj file from your repo. Make sure its the src file, not the ref or test file. + Known locations: + > Runtime: %SourceRepos%\runtime\src\libraries\\src\.csproj + > CoreCLR: %SourceRepos%\runtime\src\coreclr\src\System.Private.CoreLib\System.Private.CoreLib.csproj + > WPF: %SourceRepos%\wpf\src\Microsoft.DotNet.Wpf\src\\.csproj + > WinForms: %SourceRepos%\winforms\src\\src\.csproj + > WCF: %SourceRepos%\wcf\src\\ + Usage example: + -SourceCode ""%SourceRepos%\runtime\src\libraries\System.IO.FileSystem\"",%SourceRepos%\runtime\src\coreclr\src\System.Private.CoreLib\ + + OPTIONAL + ------------------------------------------------------------ + | PARAMETER | TYPE | DESCRIPTION | + ------------------------------------------------------------ + + -h | -Help no arguments Displays this help message. If used, all other arguments are ignored and the program exits. + + -BinLog bool Default is false (binlog file generation is disabled). + When set to true, will output a diagnostics binlog file if using '-Direction ToTripleSlash'. + + -Direction string Default is 'ToDocs'. + Determines in which direction the comments should flow. + Possible values: + > ToDocs: Comments are ported from the Intellisense xml files generated in the specified source code repo build, + to the specified Docs repo containing ECMA xml files. + > ToTripleSlash: Comments are ported from the specified Docs repo containint ECMA xml files, + to the triple slash comments on top of each API in the specified source code repo. + Usage example: + -Direction ToTripleSlash + + -DisablePrompts bool Default is false (prompts are disabled). + Avoids prompting the user for input to correct some particular errors. + Usage example: + -DisablePrompts true + + -ExceptionCollisionThreshold int (0-100) Default is 70 (If >=70% of words collide, the string is not ported). + Decides how sensitive the detection of existing exception strings should be. + The tool compares the Docs exception string with the IntelliSense xml exception string. + If the number of words found in the Docs exception is below the specified threshold, + then the IntelliSense Xml string is appended at the end of the Docs string. + The user is expected to verify the value. + The reason for this is that exceptions go through language review, and may contain more + than one root cause (separated by '-or-'), and there is no easy way to know if the string + has already been ported or not. + Usage example: + -ExceptionCollisionThreshold 60 + + -ExcludedAssemblies string list Default is empty (does not ignore any assemblies/namespaces). + Comma separated list (no spaces) of specific .NET assemblies/namespaces to ignore. + Usage example: + -ExcludedAssemblies System.IO.Compression,System.IO.Pipes + + -ExcludedNamespaces string list Default is empty (does not exclude any namespaces from the specified assemblies). + Comma separated list (no spaces) of specific namespaces to exclude from the specified assemblies. + Usage example: + -ExcludedNamespaces System.Runtime.Intrinsics,System.Reflection.Metadata + + -ExcludedTypes string list Default is empty (does not ignore any types). + Comma separated list (no spaces) of names of types to ignore. + Usage example: + -ExcludedTypes ArgumentException,Stream + + -IncludedNamespaces string list Default is empty (includes all namespaces from the specified assemblies). + Comma separated list (no spaces) of specific namespaces to include from the specified assemblies. + Usage example: + -IncludedNamespaces System,System.Data + + -IncludedTypes string list Default is empty (includes all types in the desired assemblies/namespaces). + Comma separated list (no spaces) of specific types to include. + Usage example: + -IncludedTypes FileStream,DirectoryInfo + + -PortExceptionsExisting bool Default is false (does not find and append existing exceptions). + Enable or disable finding, porting and appending summaries from existing exceptions. + Setting this to true can result in a lot of noise because there is + no easy way to detect if an exception summary has been ported already or not, + especially after it went through language review. + See `-ExceptionCollisionThreshold` to set the collision sensitivity. + Usage example: + -PortExceptionsExisting true + + -PortExceptionsNew bool Default is true (ports new exceptions). + Enable or disable finding and porting new exceptions. + Usage example: + -PortExceptionsNew false + + -PortMemberParams bool Default is true (ports Member parameters). + Enable or disable finding and porting Member parameters. + Usage example: + -PortMemberParams false + + -PortMemberProperties bool Default is true (ports Member properties). + Enable or disable finding and porting Member properties. + Usage example: + -PortMemberProperties false + + -PortMemberReturns bool Default is true (ports Member return values). + Enable or disable finding and porting Member return values. + Usage example: + -PortMemberReturns false + + -PortMemberRemarks bool Default is true (ports Member remarks). + Enable or disable finding and porting Member remarks. + Usage example: + -PortMemberRemarks false + + -PortMemberSummaries bool Default is true (ports Member summaries). + Enable or disable finding and porting Member summaries. + Usage example: + -PortMemberSummaries false + + -PortMemberTypeParams bool Default is true (ports Member TypeParams). + Enable or disable finding and porting Member TypeParams. + Usage example: + -PortMemberTypeParams false + + -PortTypeParams bool Default is true (ports Type Params). + Enable or disable finding and porting Type Params. + Usage example: + -PortTypeParams false + + -PortTypeRemarks bool Default is true (ports Type remarks). + Enable or disable finding and porting Type remarks. + Usage example: + -PortTypeRemarks false + + -PortTypeSummaries bool Default is true (ports Type summaries). + Enable or disable finding and porting Type summaries. + Usage example: + -PortTypeSummaries false + + -PortTypeTypeParams bool Default is true (ports Type TypeParams). + Enable or disable finding and porting Type TypeParams. + Usage example: + -PortTypeTypeParams false + + -PrintUndoc bool Default is false (prints a basic summary). + Prints a detailed summary of all the docs APIs that are undocumented. + Usage example: + -PrintUndoc true + + -Save bool Default is false (does not save changes). + Whether you want to save the changes in the dotnet-api-docs xml files. + Usage example: + -Save true + + -SkipInterfaceImplementations bool Default is false (includes interface implementations). + Whether you want the original interface documentation to be considered to fill the + undocumented API's documentation when the API itself does not provide its own documentation. + Setting this to false will include Explicit Interface Implementations as well. + Usage example: + -SkipInterfaceImplementations true + + -SkipInterfaceRemarks bool Default is true (excludes appending interface remarks). + Whether you want interface implementation remarks to be used when the API itself has no remarks. + Very noisy and generally the content in those remarks do not apply to the API that implements + the interface API. + Usage example: + -SkipInterfaceRemarks false + + "); + Warning(@" + tl;dr: To port from IntelliSense xmls to DOcs, specify these parameters: + + -Docs + -IntelliSense [,,...,] + -IncludedAssemblies [,,...] + -Save true + + Example: + DocsPortingTool \ + -Docs D:\dotnet-api-docs\xml \ + -IntelliSense D:\runtime\artifacts\bin\System.IO.FileSystem\ \ + -IncludedAssemblies System.IO.FileSystem,System.Runtime.Intrinsics \ + -Save true +"); + Magenta(@" + Note: + If the names of your assemblies differ from the namespaces wheres your APIs live, specify the -IncludedNamespaces argument too. + + "); + } + } +} \ No newline at end of file diff --git a/Libraries/ToDocsPorter.cs b/Libraries/ToDocsPorter.cs new file mode 100644 index 0000000..38b2d10 --- /dev/null +++ b/Libraries/ToDocsPorter.cs @@ -0,0 +1,896 @@ +#nullable enable +using Libraries.Docs; +using Libraries.IntelliSenseXml; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Libraries +{ + public class ToDocsPorter + { + private readonly Configuration Config; + private readonly DocsCommentsContainer DocsComments; + private readonly IntelliSenseXmlCommentsContainer IntelliSenseXmlComments; + + private readonly List ModifiedFiles = new List(); + private readonly List ModifiedTypes = new List(); + private readonly List ModifiedAPIs = new List(); + private readonly List ProblematicAPIs = new List(); + private readonly List AddedExceptions = new List(); + + private int TotalModifiedIndividualElements = 0; + + public ToDocsPorter(Configuration config) + { + if (config.Direction != Configuration.PortingDirection.ToDocs) + { + throw new InvalidOperationException($"Unexpected porting direction: {config.Direction}"); + } + Config = config; + DocsComments = new DocsCommentsContainer(config); + IntelliSenseXmlComments = new IntelliSenseXmlCommentsContainer(config); + + } + + public void Start() + { + IntelliSenseXmlComments.CollectFiles(); + + if (!IntelliSenseXmlComments.Members.Any()) + { + Log.ErrorAndExit("No IntelliSense xml comments found."); + } + + DocsComments.CollectFiles(); + if (!DocsComments.Types.Any()) + { + Log.ErrorAndExit("No Docs Type APIs found."); + } + + PortMissingComments(); + + PrintUndocumentedAPIs(); + PrintSummary(); + + DocsComments.Save(); + } + + private void PortMissingComments() + { + Log.Info("Looking for IntelliSense xml comments that can be ported..."); + + foreach (DocsType dTypeToUpdate in DocsComments.Types) + { + PortMissingCommentsForType(dTypeToUpdate); + } + + foreach (DocsMember dMemberToUpdate in DocsComments.Members) + { + PortMissingCommentsForMember(dMemberToUpdate); + } + } + + // Tries to find an IntelliSense xml element from which to port documentation for the specified Docs type. + private void PortMissingCommentsForType(DocsType dTypeToUpdate) + { + IntelliSenseXmlMember? tsTypeToPort = IntelliSenseXmlComments.Members.FirstOrDefault(x => x.Name == dTypeToUpdate.DocIdEscaped); + if (tsTypeToPort != null) + { + if (tsTypeToPort.Name == dTypeToUpdate.DocIdEscaped) + { + TryPortMissingSummaryForAPI(dTypeToUpdate, tsTypeToPort, null); + TryPortMissingRemarksForAPI(dTypeToUpdate, tsTypeToPort, null, skipInterfaceRemarks: true); + TryPortMissingParamsForAPI(dTypeToUpdate, tsTypeToPort, null); // Some types, like delegates, have params + TryPortMissingTypeParamsForAPI(dTypeToUpdate, tsTypeToPort, null); // Type names ending with have TypeParams + } + + if (dTypeToUpdate.Changed) + { + ModifiedTypes.AddIfNotExists(dTypeToUpdate.DocId); + ModifiedFiles.AddIfNotExists(dTypeToUpdate.FilePath); + } + } + } + + // Tries to find an IntelliSense xml element from which to port documentation for the specified Docs member. + private void PortMissingCommentsForMember(DocsMember dMemberToUpdate) + { + string docId = dMemberToUpdate.DocIdEscaped; + IntelliSenseXmlMember? tsMemberToPort = IntelliSenseXmlComments.Members.FirstOrDefault(x => x.Name == docId); + TryGetEIIMember(dMemberToUpdate, out DocsMember? interfacedMember); + + if (tsMemberToPort != null || interfacedMember != null) + { + TryPortMissingSummaryForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember); + TryPortMissingRemarksForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember, Config.SkipInterfaceRemarks); + TryPortMissingParamsForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember); + TryPortMissingTypeParamsForAPI(dMemberToUpdate, tsMemberToPort, interfacedMember); + TryPortMissingExceptionsForMember(dMemberToUpdate, tsMemberToPort); + + // Properties sometimes don't have a but have a + if (dMemberToUpdate.MemberType == "Property") + { + TryPortMissingPropertyForMember(dMemberToUpdate, tsMemberToPort, interfacedMember); + } + else if (dMemberToUpdate.MemberType == "Method") + { + TryPortMissingMethodForMember(dMemberToUpdate, tsMemberToPort, interfacedMember); + } + + if (dMemberToUpdate.Changed) + { + ModifiedAPIs.AddIfNotExists(dMemberToUpdate.DocId); + ModifiedFiles.AddIfNotExists(dMemberToUpdate.FilePath); + } + } + } + + // Gets a string indicating if an API is an explicit interface implementation, or empty. + private string GetIsEII(bool isEII) + { + return isEII ? " (EII) " : string.Empty; + } + + // Gets a string indicating if an API was created, otherwise it was modified. + private string GetIsCreated(bool created) + { + return created ? "Created" : "Modified"; + } + + // Attempts to obtain the member of the implemented interface. + private bool TryGetEIIMember(IDocsAPI dApiToUpdate, out DocsMember? interfacedMember) + { + interfacedMember = null; + + if (!Config.SkipInterfaceImplementations && dApiToUpdate is DocsMember member) + { + string interfacedMemberDocId = member.ImplementsInterfaceMember; + if (!string.IsNullOrEmpty(interfacedMemberDocId)) + { + interfacedMember = DocsComments.Members.FirstOrDefault(x => x.DocId == interfacedMemberDocId); + return interfacedMember != null; + } + } + + return false; + } + + // Ports the summary for the specified API if the field is undocumented. + private void TryPortMissingSummaryForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember) + { + if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeSummaries || + dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberSummaries) + { + return; + } + + // Only port if undocumented in MS Docs + if (dApiToUpdate.Summary.IsDocsEmpty()) + { + bool isEII = false; + + string name = string.Empty; + string value = string.Empty; + + // Try to port IntelliSense xml comments + if (tsMemberToPort != null && !tsMemberToPort.Summary.IsDocsEmpty()) + { + dApiToUpdate.Summary = tsMemberToPort.Summary; + name = tsMemberToPort.Name; + value = tsMemberToPort.Summary; + } + // or try to find if it implements a documented interface + else if (interfacedMember != null && !interfacedMember.Summary.IsDocsEmpty()) + { + dApiToUpdate.Summary = interfacedMember.Summary; + isEII = true; + name = interfacedMember.MemberName; + value = interfacedMember.Summary; + } + + if (!value.IsDocsEmpty()) + { + // Any member can have an empty summary + string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} summary: {name.Escaped()} = {value.Escaped()}"; + PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); + TotalModifiedIndividualElements++; + } + } + } + + // Ports the remarks for the specified API if the field is undocumented. + private void TryPortMissingRemarksForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember, bool skipInterfaceRemarks) + { + if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeRemarks || + dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberRemarks) + { + return; + } + + if (dApiToUpdate.Remarks.IsDocsEmpty()) + { + bool isEII = false; + string name = string.Empty; + string value = string.Empty; + + // Try to port IntelliSense xml comments + if (tsMemberToPort != null && !tsMemberToPort.Remarks.IsDocsEmpty()) + { + dApiToUpdate.Remarks = tsMemberToPort.Remarks; + name = tsMemberToPort.Name; + value = tsMemberToPort.Remarks; + } + // or try to find if it implements a documented interface + // which only happens in docs members (types have a null interfacedMember passed) + else if (interfacedMember != null && !interfacedMember.Remarks.IsDocsEmpty()) + { + DocsMember memberToUpdate = (DocsMember)dApiToUpdate; + + // Only attempt to port if the member name is the same as the interfaced member docid without prefix + if (memberToUpdate.MemberName == interfacedMember.DocId[2..]) + { + string dMemberToUpdateTypeDocIdNoPrefix = memberToUpdate.ParentType.DocId[2..]; + string interfacedMemberTypeDocIdNoPrefix = interfacedMember.ParentType.DocId[2..]; + + // Special text for EIIs in Remarks + string eiiMessage = $"This member is an explicit interface member implementation. It can be used only when the instance is cast to an interface.{Environment.NewLine + Environment.NewLine}"; + + string cleanedInterfaceRemarks = string.Empty; + if (!interfacedMember.Remarks.Contains(Configuration.ToBeAdded)) + { + cleanedInterfaceRemarks = interfacedMember.Remarks.RemoveSubstrings("##Remarks", "## Remarks", ""); + } + + // Only port the interface remarks if the user desired that + if (!skipInterfaceRemarks) + { + dApiToUpdate.Remarks = eiiMessage + cleanedInterfaceRemarks; + } + // Otherwise, always add the EII special message + else + { + dApiToUpdate.Remarks = eiiMessage; + } + + name = interfacedMember.MemberName; + value = dApiToUpdate.Remarks; + + isEII = true; + } + } + + if (!value.IsDocsEmpty()) + { + // Any member can have an empty remark + string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} remarks: {name.Escaped()} = {value.Escaped()}"; + PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); + TotalModifiedIndividualElements++; + } + } + } + + // Ports all the parameter descriptions for the specified API if any of them is undocumented. + private void TryPortMissingParamsForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember) + { + if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeParams || + dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberParams) + { + return; + } + + bool created; + bool isEII; + string name; + string value; + + if (tsMemberToPort != null) + { + foreach (DocsParam dParam in dApiToUpdate.Params) + { + if (dParam.Value.IsDocsEmpty()) + { + created = false; + isEII = false; + name = string.Empty; + value = string.Empty; + + IntelliSenseXmlParam? tsParam = tsMemberToPort.Params.FirstOrDefault(x => x.Name == dParam.Name); + + // When not found, it's a bug in Docs (param name not the same as source/ref), so need to ask the user to indicate correct name + if (tsParam == null) + { + ProblematicAPIs.AddIfNotExists($"Param=[{dParam.Name}] in Member DocId=[{dApiToUpdate.DocId}]"); + + if (tsMemberToPort.Params.Count() == 0) + { + ProblematicAPIs.AddIfNotExists($"Param=[{dParam.Name}] in Member DocId=[{dApiToUpdate.DocId}]"); + Log.Warning($" There were no IntelliSense xml comments for param '{dParam.Name}' in {dApiToUpdate.DocId}"); + } + else + { + created = TryPromptParam(dParam, tsMemberToPort, out IntelliSenseXmlParam? newTsParam); + if (newTsParam == null) + { + Log.Error($" There param '{dParam.Name}' was not found in IntelliSense xml for {dApiToUpdate.DocId}"); + } + else + { + // Now attempt to document it + if (!newTsParam.Value.IsDocsEmpty()) + { + // try to port IntelliSense xml comments + dParam.Value = newTsParam.Value; + name = newTsParam.Name; + value = newTsParam.Value; + } + // or try to find if it implements a documented interface + else if (interfacedMember != null) + { + DocsParam? interfacedParam = interfacedMember.Params.FirstOrDefault(x => x.Name == newTsParam.Name || x.Name == dParam.Name); + if (interfacedParam != null) + { + dParam.Value = interfacedParam.Value; + name = interfacedParam.Name; + value = interfacedParam.Value; + isEII = true; + } + } + } + } + } + // Attempt to port + else if (!tsParam.Value.IsDocsEmpty()) + { + // try to port IntelliSense xml comments + dParam.Value = tsParam.Value; + name = tsParam.Name; + value = tsParam.Value; + } + // or try to find if it implements a documented interface + else if (interfacedMember != null) + { + DocsParam? interfacedParam = interfacedMember.Params.FirstOrDefault(x => x.Name == dParam.Name); + if (interfacedParam != null) + { + dParam.Value = interfacedParam.Value; + name = interfacedParam.Name; + value = interfacedParam.Value; + isEII = true; + } + } + + + if (!value.IsDocsEmpty()) + { + string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} ({GetIsCreated(created)}) param {name.Escaped()} = {value.Escaped()}"; + PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); + TotalModifiedIndividualElements++; + } + } + } + } + else if (interfacedMember != null) + { + foreach (DocsParam dParam in dApiToUpdate.Params) + { + if (dParam.Value.IsDocsEmpty()) + { + DocsParam? interfacedParam = interfacedMember.Params.FirstOrDefault(x => x.Name == dParam.Name); + if (interfacedParam != null && !interfacedParam.Value.IsDocsEmpty()) + { + dParam.Value = interfacedParam.Value; + + string message = $"{dApiToUpdate.Kind} EII ({GetIsCreated(false)}) param {dParam.Name.Escaped()} = {dParam.Value.Escaped()}"; + PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId); + TotalModifiedIndividualElements++; + } + } + } + } + } + + // Ports all the type parameter descriptions for the specified API if any of them is undocumented. + private void TryPortMissingTypeParamsForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember) + { + if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeTypeParams || + dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberTypeParams) + { + return; + } + + if (tsMemberToPort != null) + { + foreach (IntelliSenseXmlTypeParam tsTypeParam in tsMemberToPort.TypeParams) + { + bool isEII = false; + string name = string.Empty; + string value = string.Empty; + + DocsTypeParam? dTypeParam = dApiToUpdate.TypeParams.FirstOrDefault(x => x.Name == tsTypeParam.Name); + + bool created = false; + if (dTypeParam == null) + { + ProblematicAPIs.AddIfNotExists($"TypeParam=[{tsTypeParam.Name}] in Member=[{dApiToUpdate.DocId}]"); + dTypeParam = dApiToUpdate.AddTypeParam(tsTypeParam.Name, XmlHelper.GetNodesInPlainText(tsTypeParam.XETypeParam)); + created = true; + } + + // But it can still be empty, try to retrieve it + if (dTypeParam.Value.IsDocsEmpty()) + { + // try to port IntelliSense xml comments + if (!tsTypeParam.Value.IsDocsEmpty()) + { + name = tsTypeParam.Name; + value = tsTypeParam.Value; + } + // or try to find if it implements a documented interface + else if (interfacedMember != null) + { + DocsTypeParam? interfacedTypeParam = interfacedMember.TypeParams.FirstOrDefault(x => x.Name == dTypeParam.Name); + if (interfacedTypeParam != null) + { + name = interfacedTypeParam.Name; + value = interfacedTypeParam.Value; + isEII = true; + } + } + } + + if (!value.IsDocsEmpty()) + { + dTypeParam.Value = value; + string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} ({GetIsCreated(created)}) typeparam {name.Escaped()} = {value.Escaped()}"; + PrintModifiedMember(message, dTypeParam.ParentAPI.FilePath, dApiToUpdate.DocId); + TotalModifiedIndividualElements++; + } + } + } + } + + // Tries to document the passed property. + private void TryPortMissingPropertyForMember(DocsMember dMemberToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember) + { + if (!Config.PortMemberProperties) + { + return; + } + + if (dMemberToUpdate.Value.IsDocsEmpty()) + { + string name = string.Empty; + string value = string.Empty; + bool isEII = false; + + // Issue: sometimes properties have their TS string in Value, sometimes in Returns + if (tsMemberToPort != null) + { + name = tsMemberToPort.Name; + if (!tsMemberToPort.Value.IsDocsEmpty()) + { + value = tsMemberToPort.Value; + } + else if (!tsMemberToPort.Returns.IsDocsEmpty()) + { + value = tsMemberToPort.Returns; + } + } + // or try to find if it implements a documented interface + else if (interfacedMember != null) + { + name = interfacedMember.MemberName; + if (!interfacedMember.Value.IsDocsEmpty()) + { + value = interfacedMember.Value; + } + else if (!interfacedMember.Returns.IsDocsEmpty()) + { + value = interfacedMember.Returns; + } + if (!string.IsNullOrEmpty(value)) + { + isEII = true; + } + } + + if (!value.IsDocsEmpty()) + { + dMemberToUpdate.Value = value; + string message = $"Member {GetIsEII(isEII)} property {name.Escaped()} = {value.Escaped()}"; + PrintModifiedMember(message, dMemberToUpdate.FilePath,dMemberToUpdate.DocId); + TotalModifiedIndividualElements++; + } + } + } + + // Tries to document the passed method. + private void TryPortMissingMethodForMember(DocsMember dMemberToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember) + { + if (!Config.PortMemberReturns) + { + return; + } + + if (dMemberToUpdate.Returns.IsDocsEmpty()) + { + string name = string.Empty; + string value = string.Empty; + bool isEII = false; + + // Bug: Sometimes a void return value shows up as not documented, skip those + if (dMemberToUpdate.ReturnType == "System.Void") + { + ProblematicAPIs.AddIfNotExists($"Unexpected System.Void return value in Method=[{dMemberToUpdate.DocId}]"); + } + else if (tsMemberToPort != null && !tsMemberToPort.Returns.IsDocsEmpty()) + { + name = tsMemberToPort.Name; + value = tsMemberToPort.Returns; + } + else if (interfacedMember != null && !interfacedMember.Returns.IsDocsEmpty()) + { + name = interfacedMember.MemberName; + value = interfacedMember.Returns; + isEII = true; + } + + if (!value.IsDocsEmpty()) + { + dMemberToUpdate.Returns = value; + string message = $"Method {GetIsEII(isEII)} returns {name.Escaped()} = {value.Escaped()}"; + PrintModifiedMember(message, dMemberToUpdate.FilePath, dMemberToUpdate.DocId); + TotalModifiedIndividualElements++; + } + } + } + + // Ports all the exceptions for the specified API. + // They are only processed if the user specified in the command arguments to NOT skip exceptions. + // All exceptions get ported, because there is no easy way to determine if an exception is already documented or not. + private void TryPortMissingExceptionsForMember(DocsMember dMemberToUpdate, IntelliSenseXmlMember? tsMemberToPort) + { + if (!Config.PortExceptionsExisting && !Config.PortExceptionsNew) + { + return; + } + + if (tsMemberToPort != null) + { + // Exceptions are a special case: If a new one is found in code, but does not exist in docs, the whole element needs to be added + foreach (IntelliSenseXmlException tsException in tsMemberToPort.Exceptions) + { + DocsException? dException = dMemberToUpdate.Exceptions.FirstOrDefault(x => x.Cref == tsException.Cref); + bool created = false; + + // First time adding the cref + if (dException == null && Config.PortExceptionsNew) + { + AddedExceptions.AddIfNotExists($"Exception=[{tsException.Cref}] in Member=[{dMemberToUpdate.DocId}]"); + string text = XmlHelper.ReplaceExceptionPatterns(XmlHelper.GetNodesInPlainText(tsException.XEException)); + dException = dMemberToUpdate.AddException(tsException.Cref, text); + created = true; + } + // If cref exists, check if the text has already been appended + else if (dException != null && Config.PortExceptionsExisting) + { + XElement formattedException = tsException.XEException; + string value = XmlHelper.ReplaceExceptionPatterns(XmlHelper.GetNodesInPlainText(formattedException)); + if (!dException.WordCountCollidesAboveThreshold(value, Config.ExceptionCollisionThreshold)) + { + AddedExceptions.AddIfNotExists($"Exception=[{tsException.Cref}] in Member=[{dMemberToUpdate.DocId}]"); + dException.AppendException(value); + created = true; + } + } + + if (dException != null) + { + if (created || (!tsException.Value.IsDocsEmpty() && dException.Value.IsDocsEmpty())) + { + string message = string.Format($"Exception ({GetIsCreated(created)}) {dException.Cref.Escaped()} = {dException.Value.Escaped()}"); + PrintModifiedMember(message, dException.ParentAPI.FilePath, dException.Cref); + + TotalModifiedIndividualElements++; + } + } + } + } + } + + // If a Param is found in a DocsType or a DocsMember that did not exist in the IntelliSense xml member, it's possible the param was unexpectedly saved in the IntelliSense xml comments with a different name, so the user gets prompted to look for it. + private bool TryPromptParam(DocsParam oldDParam, IntelliSenseXmlMember tsMember, out IntelliSenseXmlParam? newTsParam) + { + newTsParam = null; + + if (Config.DisablePrompts) + { + Log.Error($"Prompts disabled. Will not process the '{oldDParam.Name}' param."); + return false; + } + + bool created = false; + int option = -1; + while (option == -1) + { + Log.Error($"Problem in param '{oldDParam.Name}' in member '{tsMember.Name}' in file '{oldDParam.ParentAPI.FilePath}'"); + Log.Error($"The param probably exists in code, but the exact name was not found in Docs. What would you like to do?"); + Log.Warning(" 0 - Exit program."); + Log.Info(" 1 - Select the correct IntelliSense xml param from the existing ones."); + Log.Info(" 2 - Ignore this param."); + Log.Warning(" Note:Make sure to double check the affected Docs file after the tool finishes executing."); + Log.Cyan(false, "Your answer [0,1,2]: "); + + if (!int.TryParse(Console.ReadLine(), out option)) + { + Log.Error("Not a number. Try again."); + option = -1; + } + else + { + switch (option) + { + case 0: + { + Log.Info("Goodbye!"); + Environment.Exit(0); + break; + } + + case 1: + { + int paramSelection = -1; + while (paramSelection == -1) + { + Log.Info($"IntelliSense xml params found in member '{tsMember.Name}':"); + Log.Warning(" 0 - Exit program."); + int paramCounter = 1; + foreach (IntelliSenseXmlParam param in tsMember.Params) + { + Log.Info($" {paramCounter} - {param.Name}"); + paramCounter++; + } + + Log.Cyan(false, $"Your answer to match param '{oldDParam.Name}'? [0..{paramCounter - 1}]: "); + + if (!int.TryParse(Console.ReadLine(), out paramSelection)) + { + Log.Error("Not a number. Try again."); + paramSelection = -1; + } + else if (paramSelection < 0 || paramSelection >= paramCounter) + { + Log.Error("Invalid selection. Try again."); + paramSelection = -1; + } + else if (paramSelection == 0) + { + Log.Info("Goodbye!"); + Environment.Exit(0); + } + else + { + newTsParam = tsMember.Params[paramSelection - 1]; + Log.Success($"Selected: {newTsParam.Name}"); + } + } + + break; + } + + case 2: + { + Log.Info("Skipping this param."); + break; + } + + default: + { + Log.Error("Invalid selection. Try again."); + option = -1; + break; + } + } + } + } + + return created; + } + + /// + /// Standard formatted print message for a modified element. + /// + /// The friendly description of the modified API. + /// The file where the modified API lives. + /// The API unique identifier. + private void PrintModifiedMember(string message, string docsFilePath, string docId) + { + Log.Warning($" File: {docsFilePath}"); + Log.Warning($" DocID: {docId}"); + Log.Warning($" {message}"); + Log.Info("---------------------------------------------------"); + Log.Line(); + } + + // Prints all the undocumented APIs. + // This is only done if the user specified in the command arguments to print undocumented APIs. + private void PrintUndocumentedAPIs() + { + if (Config.PrintUndoc) + { + Log.Line(); + Log.Success("-----------------"); + Log.Success("UNDOCUMENTED APIS"); + Log.Success("-----------------"); + + Log.Line(); + + void TryPrintType(ref bool undocAPI, string typeDocId) + { + if (!undocAPI) + { + Log.Info(" Type: {0}", typeDocId); + undocAPI = true; + } + }; + + void TryPrintMember(ref bool undocMember, string memberDocId) + { + if (!undocMember) + { + Log.Info(" {0}", memberDocId); + undocMember = true; + } + }; + + int typeSummaries = 0; + int memberSummaries = 0; + int memberValues = 0; + int memberReturns = 0; + int memberParams = 0; + int memberTypeParams = 0; + int exceptions = 0; + + Log.Info("Undocumented APIs:"); + + foreach (DocsType docsType in DocsComments.Types) + { + bool undocAPI = false; + if (docsType.Summary.IsDocsEmpty()) + { + TryPrintType(ref undocAPI, docsType.DocId); + Log.Error($" Type Summary: {docsType.Summary}"); + typeSummaries++; + } + } + + foreach (DocsMember member in DocsComments.Members) + { + bool undocMember = false; + + if (member.Summary.IsDocsEmpty()) + { + TryPrintMember(ref undocMember, member.DocId); + + Log.Error($" Member Summary: {member.Summary}"); + memberSummaries++; + } + + if (member.MemberType == "Property") + { + if (member.Value == Configuration.ToBeAdded) + { + TryPrintMember(ref undocMember, member.DocId); + + Log.Error($" Property Value: {member.Value}"); + memberValues++; + } + } + else if (member.MemberType == "Method") + { + if (member.Returns == Configuration.ToBeAdded) + { + TryPrintMember(ref undocMember, member.DocId); + + Log.Error($" Method Returns: {member.Returns}"); + memberReturns++; + } + } + + foreach (DocsParam param in member.Params) + { + if (param.Value.IsDocsEmpty()) + { + TryPrintMember(ref undocMember, member.DocId); + + Log.Error($" Member Param: {param.Name}: {param.Value}"); + memberParams++; + } + } + + foreach (DocsTypeParam typeParam in member.TypeParams) + { + if (typeParam.Value.IsDocsEmpty()) + { + TryPrintMember(ref undocMember, member.DocId); + + Log.Error($" Member Type Param: {typeParam.Name}: {typeParam.Value}"); + memberTypeParams++; + } + } + + foreach (DocsException exception in member.Exceptions) + { + if (exception.Value.IsDocsEmpty()) + { + TryPrintMember(ref undocMember, member.DocId); + + Log.Error($" Member Exception: {exception.Cref}: {exception.Value}"); + exceptions++; + } + } + } + + Log.Info($" Undocumented type summaries: {typeSummaries}"); + Log.Info($" Undocumented member summaries: {memberSummaries}"); + Log.Info($" Undocumented method returns: {memberReturns}"); + Log.Info($" Undocumented property values: {memberValues}"); + Log.Info($" Undocumented member params: {memberParams}"); + Log.Info($" Undocumented member type params: {memberTypeParams}"); + Log.Info($" Undocumented exceptions: {exceptions}"); + + Log.Line(); + } + } + + // Prints a final summary of the execution findings. + private void PrintSummary() + { + Log.Line(); + Log.Success("---------"); + Log.Success("FINISHED!"); + Log.Success("---------"); + + Log.Line(); + Log.Info($"Total modified files: {ModifiedFiles.Count}"); + foreach (string file in ModifiedFiles) + { + Log.Success($" - {file}"); + } + + Log.Line(); + Log.Info($"Total modified types: {ModifiedTypes.Count}"); + foreach (string type in ModifiedTypes) + { + Log.Success($" - {type}"); + } + + Log.Line(); + Log.Info($"Total modified APIs: {ModifiedAPIs.Count}"); + foreach (string api in ModifiedAPIs) + { + Log.Success($" - {api}"); + } + + Log.Line(); + Log.Info($"Total problematic APIs: {ProblematicAPIs.Count}"); + foreach (string api in ProblematicAPIs) + { + Log.Warning($" - {api}"); + } + + Log.Line(); + Log.Info($"Total added exceptions: {AddedExceptions.Count}"); + foreach (string exception in AddedExceptions) + { + Log.Success($" - {exception}"); + } + + Log.Line(); + Log.Info(false, "Total modified individual elements: "); + Log.Success($"{TotalModifiedIndividualElements}"); + } + } +} diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs new file mode 100644 index 0000000..382c84b --- /dev/null +++ b/Libraries/ToTripleSlashPorter.cs @@ -0,0 +1,133 @@ +#nullable enable +using Libraries.Docs; +using Libraries.RoslynTripleSlash; +using Microsoft.Build.Logging; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.MSBuild; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Libraries +{ + public class ToTripleSlashPorter + { + private readonly Configuration Config; + private readonly DocsCommentsContainer DocsComments; + + public ToTripleSlashPorter(Configuration config) + { + if (config.Direction != Configuration.PortingDirection.ToTripleSlash) + { + throw new InvalidOperationException($"Unexpected porting direction: {config.Direction}"); + } + Config = config; + DocsComments = new DocsCommentsContainer(config); + } + + public void Start() + { + DocsComments.CollectFiles(); + if (!DocsComments.Types.Any()) + { + Log.ErrorAndExit("No Docs Type APIs found."); + } + + Log.Info("Porting from Docs to triple slash..."); + + MSBuildWorkspace workspace; + try + { + workspace = MSBuildWorkspace.Create(); + } + catch (ReflectionTypeLoadException) + { + Log.ErrorAndExit("The MSBuild directory was not found in PATH. Use '-MSBuild ' to specify it."); + return; + } + + BinaryLogger? binLogger = null; + if (Config.BinLogger) + { + binLogger = new BinaryLogger() + { + Parameters = Path.Combine(Environment.CurrentDirectory, Config.BinLogPath), + Verbosity = Microsoft.Build.Framework.LoggerVerbosity.Diagnostic, + CollectProjectImports = BinaryLogger.ProjectImportsCollectionMode.Embed + }; + } + + Project? project = workspace.OpenProjectAsync(Config.CsProj!.FullName, msbuildLogger: binLogger).Result; + if (project == null) + { + Log.ErrorAndExit("Could not find a project."); + return; + } + + Compilation? compilation = project.GetCompilationAsync().Result; + if (compilation == null) + { + throw new NullReferenceException("The project's compilation was null."); + } + + ImmutableList diagnostics = workspace.Diagnostics; + if (diagnostics.Any()) + { + foreach (var diagnostic in diagnostics) + { + Log.Error($"{diagnostic.Kind} - {diagnostic.Message}"); + } + Log.ErrorAndExit("Exiting due to diagnostic errors found."); + } + + PortCommentsForAPIs(compilation!); + } + + private void PortCommentsForAPIs(Compilation compilation) + { + foreach (DocsType docsType in DocsComments.Types) + { + INamedTypeSymbol? typeSymbol = + compilation.GetTypeByMetadataName(docsType.FullName) ?? + compilation.Assembly.GetTypeByMetadataName(docsType.FullName); + + if (typeSymbol == null) + { + Log.Warning($"Type symbol not found in compilation: {docsType.DocId}"); + continue; + } + + PortAPI(compilation, docsType, typeSymbol); + } + } + + private void PortAPI(Compilation compilation, IDocsAPI api, ISymbol symbol) + { + bool useBoilerplate = false; + foreach (Location location in symbol.Locations) + { + SyntaxTree? tree = location.SourceTree; + if (tree == null) + { + Log.Warning($"Tree not found for location of {symbol.Name}"); + continue; + } + + SemanticModel model = compilation.GetSemanticModel(tree); + var rewriter = new TripleSlashSyntaxRewriter(DocsComments, model, location, tree, useBoilerplate); + SyntaxNode? newRoot = rewriter.Visit(tree.GetRoot()); + if (newRoot == null) + { + Log.Warning($"New returned root is null for {api.DocId} in {tree.FilePath}"); + continue; + } + + File.WriteAllText(tree.FilePath, newRoot.ToFullString()); + useBoilerplate = true; + } + } + } +} diff --git a/Libraries/XmlHelper.cs b/Libraries/XmlHelper.cs new file mode 100644 index 0000000..cbf6a6c --- /dev/null +++ b/Libraries/XmlHelper.cs @@ -0,0 +1,320 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Linq; + +namespace Libraries +{ + 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" }, + }; + + public static string GetAttributeValue(XElement parent, string name) + { + if (parent == null) + { + Log.Error("A null parent was passed when attempting to get attribute '{0}'", name); + throw new ArgumentNullException(nameof(parent)); + } + else + { + XAttribute attr = parent.Attribute(name); + if (attr != null) + { + return attr.Value.Trim(); + } + } + return string.Empty; + } + + public static bool TryGetChildElement(XElement parent, string name, out XElement? child) + { + child = null; + + if (parent == null || string.IsNullOrWhiteSpace(name)) + return false; + + child = parent.Element(name); + + return child != null; + } + + public static string GetChildElementValue(XElement parent, string childName) + { + XElement child = parent.Element(childName); + + if (child != null) + { + return GetNodesInPlainText(child); + } + + return string.Empty; + } + + public static string GetNodesInPlainText(XElement element) + { + if (element == null) + { + Log.Error("A null element was passed when attempting to retrieve the nodes in plain text."); + throw new ArgumentNullException(nameof(element)); + } + return string.Join("", element.Nodes()).Trim(); + } + + public static void SaveFormattedAsMarkdown(XElement element, string newValue, bool isMember) + { + if (element == null) + { + Log.Error("A null element was passed when attempting to save formatted as markdown"); + throw new ArgumentNullException(nameof(element)); + } + + // 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 = RemoveUndesiredEndlines(newValue); + updatedValue = SubstituteRemarksRegexPatterns(updatedValue); + updatedValue = ReplaceMarkdownPatterns(updatedValue); + + 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) + { + Log.Error("A null parent was passed when attempting to add child formatted as markdown"); + throw new ArgumentNullException(nameof(parent)); + } + + if (child == null) + { + Log.Error("A null child was passed when attempting to add child formatted as markdown"); + throw new ArgumentNullException(nameof(child)); + } + + SaveFormattedAsMarkdown(child, childValue, isMember); + parent.Add(child); + } + + public static void SaveFormattedAsXml(XElement element, string newValue, bool removeUndesiredEndlines = true) + { + if (element == null) + { + Log.Error("A null element was passed when attempting to save formatted as xml"); + throw new ArgumentNullException(nameof(element)); + } + + 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) + { + Log.Error("A null element was passed when attempting to append formatted as xml"); + throw new ArgumentNullException(nameof(element)); + } + + SaveFormattedAsXml(element, GetNodesInPlainText(element) + valueToAppend, removeUndesiredEndlines); + } + + public static void AddChildFormattedAsXml(XElement parent, XElement child, string childValue) + { + if (parent == null) + { + Log.Error("A null parent was passed when attempting to add child formatted as xml"); + throw new ArgumentNullException(nameof(parent)); + } + + if (child == null) + { + Log.Error("A null child was passed when attempting to add child formatted as xml"); + throw new ArgumentNullException(nameof(child)); + } + + SaveFormattedAsXml(child, childValue); + parent.Add(child); + } + + private static string RemoveUndesiredEndlines(string value) + { + Regex regex = new Regex(@"((?'undesiredEndlinePrefix'[^\.\:])(\r\n)+[ \t]*)"); + string newValue = value; + if (regex.IsMatch(value)) + { + newValue = regex.Replace(value, @"${undesiredEndlinePrefix} "); + } + return newValue.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 (updatedValue.Contains(kvp.Key)) + { + updatedValue = updatedValue.Replace(kvp.Key, kvp.Value); + } + } + 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); + } + } + 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; + } + + private static string SubstituteRegexPatterns(string value, Dictionary replaceableRegexPatterns) + { + foreach (KeyValuePair pattern in replaceableRegexPatterns) + { + Regex regex = new Regex(pattern.Key); + if (regex.IsMatch(value)) + { + value = regex.Replace(value, pattern.Value); + } + } + + return value; + } + } +} \ No newline at end of file From 24df72e648bac3378121318575966b641913cd48 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:45:02 -0800 Subject: [PATCH 34/65] DocsPortingTool changes. --- Program/DocsPortingTool.cs | 129 +++++++++++++++++++++++++ Program/DocsPortingTool.csproj | 26 +++++ Program/Properties/launchSettings.json | 15 +++ 3 files changed, 170 insertions(+) create mode 100644 Program/DocsPortingTool.cs create mode 100644 Program/DocsPortingTool.csproj create mode 100644 Program/Properties/launchSettings.json diff --git a/Program/DocsPortingTool.cs b/Program/DocsPortingTool.cs new file mode 100644 index 0000000..92d1c70 --- /dev/null +++ b/Program/DocsPortingTool.cs @@ -0,0 +1,129 @@ +#nullable enable +using Libraries; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Loader; +using System.IO; +using System.Linq; +using Microsoft.Build.Locator; + +namespace DocsPortingTool +{ + class DocsPortingTool + { + public static void Main(string[] args) + { + Configuration config = Configuration.GetCLIArgumentsForDocsPortingTool(args); + switch (config.Direction) + { + case Configuration.PortingDirection.ToDocs: + { + var porter = new ToDocsPorter(config); + porter.Start(); + break; + } + case Configuration.PortingDirection.ToTripleSlash: + { + // This ensures we can load MSBuild property before calling the ToTripleSlashPorter constructor + VisualStudioInstance? msBuildInstance = MSBuildLocator.QueryVisualStudioInstances().First(); + Register(msBuildInstance.MSBuildPath); + MSBuildLocator.RegisterInstance(msBuildInstance); + + var porter = new ToTripleSlashPorter(config); + porter.Start(); + break; + } + default: + throw new ArgumentOutOfRangeException($"Unrecognized porting direction: {config.Direction}"); + } + } + + private static readonly Dictionary s_pathsToAssemblies = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary s_namesToAssemblies = new Dictionary(); + + private static readonly object s_guard = new object(); + + /// + /// Register an assembly loader that will load assemblies with higher version than what was requested. + /// + private static void Register(string searchPath) + { + AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext context, AssemblyName assemblyName) => + { + lock (s_guard) + { + if (s_namesToAssemblies.TryGetValue(assemblyName.FullName, out var cachedAssembly)) + { + return cachedAssembly; + } + + var assembly = TryResolveAssemblyFromPaths(context, assemblyName, searchPath, s_pathsToAssemblies); + + // Cache assembly + if (assembly != null) + { + var name = assembly.FullName; + if (name is null) + { + throw new Exception($"Could not get name for assembly '{assembly}'"); + } + + s_pathsToAssemblies[assembly.Location] = assembly; + s_namesToAssemblies[name] = assembly; + } + + return assembly; + } + }; + } + internal static Assembly? TryResolveAssemblyFromPaths(AssemblyLoadContext context, AssemblyName assemblyName, string searchPath, Dictionary? knownAssemblyPaths = null) + { + foreach (var cultureSubfolder in string.IsNullOrEmpty(assemblyName.CultureName) + // If no culture is specified, attempt to load directly from + // the known dependency paths. + ? new[] { string.Empty } + // Search for satellite assemblies in culture subdirectories + // of the assembly search directories, but fall back to the + // bare search directory if that fails. + : new[] { assemblyName.CultureName, string.Empty }) + { + foreach (var extension in new[] { "ni.dll", "ni.exe", "dll", "exe" }) + { + var candidatePath = Path.Combine( + searchPath, cultureSubfolder, $"{assemblyName.Name}.{extension}"); + + var isAssemblyLoaded = knownAssemblyPaths?.ContainsKey(candidatePath) == true; + if (isAssemblyLoaded || !File.Exists(candidatePath)) + { + continue; + } + + var candidateAssemblyName = AssemblyLoadContext.GetAssemblyName(candidatePath); + if (candidateAssemblyName.Version < assemblyName.Version) + { + continue; + } + + try + { + var assembly = context.LoadFromAssemblyPath(candidatePath); + return assembly; + } + catch + { + if (assemblyName.Name != null) + { + // We were unable to load the assembly from the file path. It is likely that + // a different version of the assembly has already been loaded into the context. + // Be forgiving and attempt to load assembly by name without specifying a version. + return context.LoadFromAssemblyName(new AssemblyName(assemblyName.Name)); + } + } + } + } + + return null; + } + } +} diff --git a/Program/DocsPortingTool.csproj b/Program/DocsPortingTool.csproj new file mode 100644 index 0000000..6efdea0 --- /dev/null +++ b/Program/DocsPortingTool.csproj @@ -0,0 +1,26 @@ + + + + Exe + net5.0 + DocsPortingTool.DocsPortingTool + Microsoft + carlossanlop + enable + true + true + 3.0.0 + + + + + + + + + + + + + + diff --git a/Program/Properties/launchSettings.json b/Program/Properties/launchSettings.json new file mode 100644 index 0000000..35956d8 --- /dev/null +++ b/Program/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "profiles": { + "Program": { + "commandName": "Project", + "commandLineArgs": "-CsProj D:\\runtime\\src\\libraries\\System.IO.Compression\\src\\System.IO.Compression.csproj -Docs D:\\dotnet-api-docs\\xml -Save true -Direction ToTripleSlash -IncludedAssemblies System.IO.Compression,System.IO.Compression.Brotli -SkipInterfaceImplementations true", + "environmentVariables": { + "DOCS_IOT": "D:\\iot\\artifacts\\bin", + "DOCS_CORECLR": "D:\\runtime\\artifacts\\bin\\coreclr\\Windows_NT.x64.Release\\IL\\", + "DOCS_WINFORMS": "D:\\winforms\\artifacts\\bin\\", + "DOCS_WPF": "D:\\wpf\\.tools\\native\\bin\\dotnet-api-docs_netcoreapp3.0\\0.0.0.1\\_intellisense\\\\netcore-3.0\\", + "DOCS_RUNTIME": "D:\\runtime\\artifacts\\bin\\" + } + } + } +} \ No newline at end of file From e2a39ccbec0799f5a895c7d35bfb9524553c0767 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:45:16 -0800 Subject: [PATCH 35/65] Solution file. --- DocsPortingTool.sln | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/DocsPortingTool.sln b/DocsPortingTool.sln index 4130fb8..84e75b9 100644 --- a/DocsPortingTool.sln +++ b/DocsPortingTool.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28705.295 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocsPortingTool", "DocsPortingTool\DocsPortingTool.csproj", "{87BBF4FD-260C-4AC4-802B-7D2B29629C07}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libraries", "Libraries\Libraries.csproj", "{87BBF4FD-260C-4AC4-802B-7D2B29629C07}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{81FEFEA4-8FF5-482E-A33D-D3F351D3F7B6}" EndProject @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocsPortingTool", "Program\DocsPortingTool.csproj", "{E92246CD-548D-4C08-BA43-594663E78100}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {81FEFEA4-8FF5-482E-A33D-D3F351D3F7B6}.Debug|Any CPU.Build.0 = Debug|Any CPU {81FEFEA4-8FF5-482E-A33D-D3F351D3F7B6}.Release|Any CPU.ActiveCfg = Release|Any CPU {81FEFEA4-8FF5-482E-A33D-D3F351D3F7B6}.Release|Any CPU.Build.0 = Release|Any CPU + {E92246CD-548D-4C08-BA43-594663E78100}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E92246CD-548D-4C08-BA43-594663E78100}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E92246CD-548D-4C08-BA43-594663E78100}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E92246CD-548D-4C08-BA43-594663E78100}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From bdd49fc45bd2db7667f3ccebb49c404ff00a624d Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:45:25 -0800 Subject: [PATCH 36/65] install as tool update. --- install-as-tool.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install-as-tool.ps1 b/install-as-tool.ps1 index cb83c32..dabcd79 100755 --- a/install-as-tool.ps1 +++ b/install-as-tool.ps1 @@ -4,7 +4,7 @@ $ErrorActionPreference = "Stop" Push-Location $(Split-Path $MyInvocation.MyCommand.Path) $ARTIFACTS_DIR = "artifacts" -$PROJECT_NAME = "DocsPortingTool" +$PROJECT_NAME = "Program" $BUILD_CONFIGURATION = "Release" dotnet pack -c $BUILD_CONFIGURATION -o $ARTIFACTS_DIR $PROJECT_NAME From ddf77592f2504568f581026da62d38d2e983e326 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 19 Nov 2020 11:45:31 -0800 Subject: [PATCH 37/65] Unit tests. --- Tests/TestData.cs | 10 +++++----- Tests/TestDirectory.cs | 2 +- Tests/Tests.cs | 41 ++++++++++++++++++++--------------------- Tests/Tests.csproj | 18 ++++++++++++------ 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/Tests/TestData.cs b/Tests/TestData.cs index 51157d8..846feef 100644 --- a/Tests/TestData.cs +++ b/Tests/TestData.cs @@ -1,7 +1,7 @@ using System.IO; using Xunit; -namespace DocsPortingTool.Tests +namespace Libraries.Tests { public class TestData { @@ -14,10 +14,10 @@ public class TestData public string Assembly { get; private set; } public string Namespace { get; private set; } public string Type { get; private set; } - public DirectoryInfo TripleSlash { get; private set; } + public DirectoryInfo IntelliSenseAndDLL { get; private set; } public DirectoryInfo Docs { get; private set; } - /// Triple slash xml file. + /// IntelliSense xml file. public string OriginalFilePath { get; private set; } /// Docs file as we should expect it to look. public string ExpectedFilePath { get; private set; } @@ -35,8 +35,8 @@ public TestData(TestDirectory tempDir, string testDataDir, string assemblyName, Namespace = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; Type = typeName; - TripleSlash = tempDir.CreateSubdirectory("TripleSlash"); - DirectoryInfo tsAssemblyDir = TripleSlash.CreateSubdirectory(Assembly); + IntelliSenseAndDLL = tempDir.CreateSubdirectory("IntelliSenseAndDLL"); + DirectoryInfo tsAssemblyDir = IntelliSenseAndDLL.CreateSubdirectory(Assembly); Docs = tempDir.CreateSubdirectory("Docs"); DirectoryInfo docsAssemblyDir = Docs.CreateSubdirectory(Namespace); diff --git a/Tests/TestDirectory.cs b/Tests/TestDirectory.cs index 75a1961..46e0a2b 100644 --- a/Tests/TestDirectory.cs +++ b/Tests/TestDirectory.cs @@ -2,7 +2,7 @@ using System.IO; using Xunit; -namespace DocsPortingTool.Tests +namespace Libraries.Tests { public class TestDirectory : IDisposable { diff --git a/Tests/Tests.cs b/Tests/Tests.cs index 92a3231..8f8c6fc 100644 --- a/Tests/Tests.cs +++ b/Tests/Tests.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; using System.IO; using Xunit; -namespace DocsPortingTool.Tests +namespace Libraries.Tests { public class Tests { @@ -10,60 +9,60 @@ public class Tests // Verifies the basic case of porting all regular fields. public void Port_Basic() { - Port("Basic"); + PortToDocs("Basic"); } [Fact] public void Port_DontAddMissingRemarks() { - Port("DontAddMissingRemarks"); + PortToDocs("DontAddMissingRemarks"); } [Fact] // Verifies porting of APIs living in namespaces whose name match their assembly. public void Port_AssemblyAndNamespaceSame() { - Port("AssemblyAndNamespaceSame"); + PortToDocs("AssemblyAndNamespaceSame"); } [Fact] // Verifies porting of APIs living in namespaces whose name does not match their assembly. public void Port_AssemblyAndNamespaceDifferent() { - Port("AssemblyAndNamespaceDifferent", + PortToDocs("AssemblyAndNamespaceDifferent", assemblyName: "MyAssembly", namespaceName: "MyNamespace"); } [Fact] - // Ports Type remarks from triple slash. - // Ports Method remarks from triple slash. + // Ports Type remarks from IntelliSense xml. + // Ports Method remarks from IntelliSense xml. // No interface strings should be ported. public void Port_Remarks_NoEII_NoInterfaceRemarks() { - Port("Remarks_NoEII_NoInterfaceRemarks", + PortToDocs("Remarks_NoEII_NoInterfaceRemarks", skipInterfaceImplementations: true, skipInterfaceRemarks: true); } [Fact] - // Ports Type remarks from triple slash. - // Ports Method remarks from triple slash. + // Ports Type remarks from IntelliSense xml. + // Ports Method remarks from IntelliSense xml. // Ports EII message and interface method remarks. public void Port_Remarks_WithEII_WithInterfaceRemarks() { - Port("Remarks_WithEII_WithInterfaceRemarks", + PortToDocs("Remarks_WithEII_WithInterfaceRemarks", skipInterfaceImplementations: false, skipInterfaceRemarks: false); } [Fact] - // Ports Type remarks from triple slash. - // Ports Method remarks from triple slash. + // Ports Type remarks from IntelliSense xml. + // Ports Method remarks from IntelliSense xml. // Ports EII message but no interface method remarks. public void Port_Remarks_WithEII_NoInterfaceRemarks() { - Port("Remarks_WithEII_NoInterfaceRemarks", + PortToDocs("Remarks_WithEII_NoInterfaceRemarks", skipInterfaceImplementations: false, skipInterfaceRemarks: true); } @@ -72,7 +71,7 @@ public void Port_Remarks_WithEII_NoInterfaceRemarks() /// Verifies that new exceptions are ported. public void Port_Exceptions() { - Port("Exceptions"); + PortToDocs("Exceptions"); } [Fact] @@ -80,12 +79,12 @@ public void Port_Exceptions() /// language review, does not get ported if its above the difference threshold. public void Port_Exception_ExistingCref() { - Port("Exception_ExistingCref", + PortToDocs("Exception_ExistingCref", portExceptionsExisting: true, exceptionCollisionThreshold: 60); } - private void Port( + private void PortToDocs( string testDataDir, bool disablePrompts = true, bool printUndoc = false, @@ -132,10 +131,10 @@ private void Port( } c.DirsDocsXml.Add(testData.Docs); - c.DirsTripleSlashXmls.Add(testData.TripleSlash); + c.DirsIntelliSense.Add(testData.IntelliSenseAndDLL); - Analyzer analyzer = new Analyzer(c); - analyzer.Start(); + var porter = new ToDocsPorter(c); + porter.Start(); Verify(testData); } diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 829934b..5b5a794 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -7,15 +7,21 @@ - - - - - + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + From fe18f74be7038718b30d258fafaa48de3a481d5e Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 20 Nov 2020 17:33:25 -0800 Subject: [PATCH 38/65] Cleanup. --- .../TripleSlashSyntaxRewriter.cs | 36 +++++++++++-------- Libraries/ToTripleSlashPorter.cs | 1 - 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index b86008d..8dfd20d 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -131,7 +131,7 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod if (!UseBoilerplate) { - if (!TryGetType(node, symbol, out DocsType? type)) + if (!TryGetType(symbol, out DocsType? type)) { return node; } @@ -239,13 +239,6 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(xmlRemarksContent); return GetXmlTrivia(xmlRemarks, leadingWhitespace); - - //DocumentationCommentTriviaSyntax triviaNode = SyntaxFactory.DocumentationComment(SyntaxKind.SingleLineDocumentationCommentTrivia, content); - //SyntaxTrivia docCommentTrivia = SyntaxFactory.Trivia(triviaNode); - - //return leadingWhitespace - //.Add(docCommentTrivia) - //.Add(SyntaxFactory.CarriageReturnLineFeed); } return new(); @@ -267,7 +260,7 @@ private SyntaxTriviaList GetParam(string name, string text, SyntaxTriviaList lea private SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace) { - var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute("name", text)); + var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute(name, text)); SyntaxList contents = GetContentsInRows(text); return GetXmlTrivia("typeparam", attribute, contents, leadingWhitespace); } @@ -333,19 +326,32 @@ private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitesp private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace) { string whitespace = leadingWhitespace.ToFullString().Replace(Environment.NewLine, ""); + SyntaxToken newLineAndWhitespace = SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace); + + SyntaxTrivia leadingTrivia = SyntaxFactory.SyntaxTrivia(SyntaxKind.DocumentationCommentExteriorTrivia, string.Empty); + SyntaxTriviaList leading = SyntaxTriviaList.Create(leadingTrivia); + var tokens = new List(); - foreach (string line in text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)) + tokens.Add(newLineAndWhitespace); + foreach (string line in text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { - tokens.Add(SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace)); // Needs to be textnewline, and below needs to be textliteral - tokens.Add(SyntaxFactory.XmlTextLiteral(SyntaxTriviaList.Create(SyntaxFactory.SyntaxTrivia(SyntaxKind.DocumentationCommentExteriorTrivia, string.Empty)), line, line, default)); + SyntaxToken token = SyntaxFactory.XmlTextLiteral(leading, line, line, default); + tokens.Add(token); + tokens.Add(newLineAndWhitespace); } - return SyntaxFactory.TokenList(tokens); } private SyntaxList GetContentsInRows(string text) { - return new(SyntaxFactory.XmlText(text)); // TODO: Press enter! + var nodes = new SyntaxList(); + foreach (string line in text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var tokenList = SyntaxFactory.ParseTokens(line).ToArray(); // Prevents unexpected change from "<" to "<" + XmlTextSyntax xmlText = SyntaxFactory.XmlText(tokenList); + return nodes.Add(xmlText); + } + return nodes; } private SyntaxTriviaList GetXmlTrivia(XmlNodeSyntax node, SyntaxTriviaList leadingWhitespace) @@ -393,7 +399,7 @@ private bool TryGetMember(SyntaxNode node, [NotNullWhen(returnValue: true)] out return member != null; } - private bool TryGetType(SyntaxNode node, ISymbol symbol, [NotNullWhen(returnValue: true)] out DocsType? type) + private bool TryGetType(ISymbol symbol, [NotNullWhen(returnValue: true)] out DocsType? type) { type = null; diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index 382c84b..242764c 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -5,7 +5,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.MSBuild; using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; From f0164acecc0397362ea478d19c4da3b15e39824e Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 5 Jan 2021 17:39:15 -0800 Subject: [PATCH 39/65] Newline fine tuning. --- .../TripleSlashSyntaxRewriter.cs | 44 ++++++++++++++++--- Libraries/ToTripleSlashPorter.cs | 8 ++-- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index 8dfd20d..8432ee4 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -25,6 +25,11 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod UseBoilerplate = useBoilerplate; } + /// + /// + /// + /// + /// public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) { SyntaxNode? baseNode = base.VisitClassDeclaration(node); @@ -233,8 +238,8 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp { if (!UseBoilerplate && !text.IsDocsEmpty()) { - string trimmedRemarks = text.RemoveSubstrings("").Trim(); - SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed)); + string trimmedRemarks = text.RemoveSubstrings("").Trim(); // The SyntaxFactory needs to add this + SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true); XmlNodeSyntax xmlRemarksContent = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(xmlRemarksContent); @@ -296,7 +301,7 @@ private SyntaxTriviaList GetExceptions(DocsMember member, SyntaxTriviaList leadi private SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace) { TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); - XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text, leadingWhitespace)); + XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text, leadingWhitespace, addInitialNewLine: false)); XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); return GetXmlTrivia(element, leadingWhitespace); } @@ -323,7 +328,7 @@ private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitesp return GetXmlTrivia(element, leadingWhitespace); } - private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace) + private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace, bool addInitialNewLine) { string whitespace = leadingWhitespace.ToFullString().Replace(Environment.NewLine, ""); SyntaxToken newLineAndWhitespace = SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace); @@ -332,12 +337,37 @@ private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhi SyntaxTriviaList leading = SyntaxTriviaList.Create(leadingTrivia); var tokens = new List(); - tokens.Add(newLineAndWhitespace); - foreach (string line in text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + + string[] splittedLines = text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + // Only add the initial new line and whitespace if the contents have more than one line. Otherwise, we want the contents to be inlined inside the tags. + if (splittedLines.Length > 1 && addInitialNewLine) + { + // For example, the remarks section needs a new line before the initial "## Remarks" title + tokens.Add(newLineAndWhitespace); + tokens.Add(newLineAndWhitespace); + } + + int lineNumber = 1; + foreach (string line in splittedLines) { SyntaxToken token = SyntaxFactory.XmlTextLiteral(leading, line, line, default); tokens.Add(token); - tokens.Add(newLineAndWhitespace); + + // Only add extra new lines if we expect more than one line of text in the contents. Otherwise, inline it inside the tags. + if (splittedLines.Length > 1) + { + tokens.Add(newLineAndWhitespace); + + if (lineNumber < splittedLines.Length) + { + // New line characters between sentences need to have their own separate line + // but need to avoid adding a final single separate line + tokens.Add(newLineAndWhitespace); + } + } + + lineNumber++; } return SyntaxFactory.TokenList(tokens); } diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index 242764c..dfabe2c 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -82,10 +82,10 @@ public void Start() Log.ErrorAndExit("Exiting due to diagnostic errors found."); } - PortCommentsForAPIs(compilation!); + PortCommentsForProject(compilation!); } - private void PortCommentsForAPIs(Compilation compilation) + private void PortCommentsForProject(Compilation compilation) { foreach (DocsType docsType in DocsComments.Types) { @@ -99,11 +99,11 @@ private void PortCommentsForAPIs(Compilation compilation) continue; } - PortAPI(compilation, docsType, typeSymbol); + PortCommentsForType(compilation, docsType, typeSymbol); } } - private void PortAPI(Compilation compilation, IDocsAPI api, ISymbol symbol) + private void PortCommentsForType(Compilation compilation, IDocsAPI api, ISymbol symbol) { bool useBoilerplate = false; foreach (Location location in symbol.Locations) From 6443fd786d3f3a84855f49cd95d07cbbd88d8818 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 5 Jan 2021 17:39:38 -0800 Subject: [PATCH 40/65] Move existing unit test code to PortToDocs folder. --- Tests/{Tests.cs => PortToDocs/PortToDocsTests.cs} | 2 +- Tests/{ => PortToDocs}/TestData.cs | 2 +- .../TestData/AssemblyAndNamespaceDifferent/DocsExpected.xml | 0 .../TestData/AssemblyAndNamespaceDifferent/DocsOriginal.xml | 0 .../TestData/AssemblyAndNamespaceDifferent/TSOriginal.xml | 0 .../TestData/AssemblyAndNamespaceSame/DocsExpected.xml | 0 .../TestData/AssemblyAndNamespaceSame/DocsOriginal.xml | 0 .../TestData/AssemblyAndNamespaceSame/TSOriginal.xml | 0 Tests/{ => PortToDocs}/TestData/Basic/DocsExpected.xml | 0 Tests/{ => PortToDocs}/TestData/Basic/DocsOriginal.xml | 0 Tests/{ => PortToDocs}/TestData/Basic/TSOriginal.xml | 0 .../TestData/DontAddMissingRemarks/DocsExpected.xml | 0 .../TestData/DontAddMissingRemarks/DocsOriginal.xml | 0 .../TestData/DontAddMissingRemarks/TSOriginal.xml | 0 .../TestData/Exception_ExistingCref/DocsExpected.xml | 0 .../TestData/Exception_ExistingCref/DocsOriginal.xml | 0 .../TestData/Exception_ExistingCref/TSOriginal.xml | 0 Tests/{ => PortToDocs}/TestData/Exceptions/DocsExpected.xml | 0 Tests/{ => PortToDocs}/TestData/Exceptions/DocsOriginal.xml | 0 Tests/{ => PortToDocs}/TestData/Exceptions/TSOriginal.xml | 0 .../TestData/Remarks_NoEII_NoInterfaceRemarks/DocsExpected.xml | 0 .../TestData/Remarks_NoEII_NoInterfaceRemarks/DocsInterface.xml | 0 .../TestData/Remarks_NoEII_NoInterfaceRemarks/DocsOriginal.xml | 0 .../TestData/Remarks_NoEII_NoInterfaceRemarks/TSOriginal.xml | 0 .../Remarks_WithEII_NoInterfaceRemarks/DocsExpected.xml | 0 .../Remarks_WithEII_NoInterfaceRemarks/DocsInterface.xml | 0 .../Remarks_WithEII_NoInterfaceRemarks/DocsOriginal.xml | 0 .../TestData/Remarks_WithEII_NoInterfaceRemarks/TSOriginal.xml | 0 .../Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml | 0 .../Remarks_WithEII_WithInterfaceRemarks/DocsInterface.xml | 0 .../Remarks_WithEII_WithInterfaceRemarks/DocsOriginal.xml | 0 .../Remarks_WithEII_WithInterfaceRemarks/TSOriginal.xml | 0 32 files changed, 2 insertions(+), 2 deletions(-) rename Tests/{Tests.cs => PortToDocs/PortToDocsTests.cs} (99%) rename Tests/{ => PortToDocs}/TestData.cs (97%) rename Tests/{ => PortToDocs}/TestData/AssemblyAndNamespaceDifferent/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/AssemblyAndNamespaceDifferent/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/AssemblyAndNamespaceDifferent/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/AssemblyAndNamespaceSame/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/AssemblyAndNamespaceSame/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/AssemblyAndNamespaceSame/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Basic/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/Basic/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Basic/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/DontAddMissingRemarks/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/DontAddMissingRemarks/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/DontAddMissingRemarks/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Exception_ExistingCref/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/Exception_ExistingCref/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Exception_ExistingCref/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Exceptions/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/Exceptions/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Exceptions/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsInterface.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_NoEII_NoInterfaceRemarks/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsInterface.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_NoInterfaceRemarks/TSOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsInterface.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsOriginal.xml (100%) rename Tests/{ => PortToDocs}/TestData/Remarks_WithEII_WithInterfaceRemarks/TSOriginal.xml (100%) diff --git a/Tests/Tests.cs b/Tests/PortToDocs/PortToDocsTests.cs similarity index 99% rename from Tests/Tests.cs rename to Tests/PortToDocs/PortToDocsTests.cs index 8f8c6fc..7e29411 100644 --- a/Tests/Tests.cs +++ b/Tests/PortToDocs/PortToDocsTests.cs @@ -3,7 +3,7 @@ namespace Libraries.Tests { - public class Tests + public class PortToDocsTests { [Fact] // Verifies the basic case of porting all regular fields. diff --git a/Tests/TestData.cs b/Tests/PortToDocs/TestData.cs similarity index 97% rename from Tests/TestData.cs rename to Tests/PortToDocs/TestData.cs index 846feef..391d7de 100644 --- a/Tests/TestData.cs +++ b/Tests/PortToDocs/TestData.cs @@ -5,7 +5,7 @@ namespace Libraries.Tests { public class TestData { - private string TestDataRootDir => @"..\..\..\TestData"; + private string TestDataRootDir => @"..\..\..\PortToDocs\TestData"; public const string TestAssembly = "MyAssembly"; public const string TestNamespace = "MyNamespace"; diff --git a/Tests/TestData/AssemblyAndNamespaceDifferent/DocsExpected.xml b/Tests/PortToDocs/TestData/AssemblyAndNamespaceDifferent/DocsExpected.xml similarity index 100% rename from Tests/TestData/AssemblyAndNamespaceDifferent/DocsExpected.xml rename to Tests/PortToDocs/TestData/AssemblyAndNamespaceDifferent/DocsExpected.xml diff --git a/Tests/TestData/AssemblyAndNamespaceDifferent/DocsOriginal.xml b/Tests/PortToDocs/TestData/AssemblyAndNamespaceDifferent/DocsOriginal.xml similarity index 100% rename from Tests/TestData/AssemblyAndNamespaceDifferent/DocsOriginal.xml rename to Tests/PortToDocs/TestData/AssemblyAndNamespaceDifferent/DocsOriginal.xml diff --git a/Tests/TestData/AssemblyAndNamespaceDifferent/TSOriginal.xml b/Tests/PortToDocs/TestData/AssemblyAndNamespaceDifferent/TSOriginal.xml similarity index 100% rename from Tests/TestData/AssemblyAndNamespaceDifferent/TSOriginal.xml rename to Tests/PortToDocs/TestData/AssemblyAndNamespaceDifferent/TSOriginal.xml diff --git a/Tests/TestData/AssemblyAndNamespaceSame/DocsExpected.xml b/Tests/PortToDocs/TestData/AssemblyAndNamespaceSame/DocsExpected.xml similarity index 100% rename from Tests/TestData/AssemblyAndNamespaceSame/DocsExpected.xml rename to Tests/PortToDocs/TestData/AssemblyAndNamespaceSame/DocsExpected.xml diff --git a/Tests/TestData/AssemblyAndNamespaceSame/DocsOriginal.xml b/Tests/PortToDocs/TestData/AssemblyAndNamespaceSame/DocsOriginal.xml similarity index 100% rename from Tests/TestData/AssemblyAndNamespaceSame/DocsOriginal.xml rename to Tests/PortToDocs/TestData/AssemblyAndNamespaceSame/DocsOriginal.xml diff --git a/Tests/TestData/AssemblyAndNamespaceSame/TSOriginal.xml b/Tests/PortToDocs/TestData/AssemblyAndNamespaceSame/TSOriginal.xml similarity index 100% rename from Tests/TestData/AssemblyAndNamespaceSame/TSOriginal.xml rename to Tests/PortToDocs/TestData/AssemblyAndNamespaceSame/TSOriginal.xml diff --git a/Tests/TestData/Basic/DocsExpected.xml b/Tests/PortToDocs/TestData/Basic/DocsExpected.xml similarity index 100% rename from Tests/TestData/Basic/DocsExpected.xml rename to Tests/PortToDocs/TestData/Basic/DocsExpected.xml diff --git a/Tests/TestData/Basic/DocsOriginal.xml b/Tests/PortToDocs/TestData/Basic/DocsOriginal.xml similarity index 100% rename from Tests/TestData/Basic/DocsOriginal.xml rename to Tests/PortToDocs/TestData/Basic/DocsOriginal.xml diff --git a/Tests/TestData/Basic/TSOriginal.xml b/Tests/PortToDocs/TestData/Basic/TSOriginal.xml similarity index 100% rename from Tests/TestData/Basic/TSOriginal.xml rename to Tests/PortToDocs/TestData/Basic/TSOriginal.xml diff --git a/Tests/TestData/DontAddMissingRemarks/DocsExpected.xml b/Tests/PortToDocs/TestData/DontAddMissingRemarks/DocsExpected.xml similarity index 100% rename from Tests/TestData/DontAddMissingRemarks/DocsExpected.xml rename to Tests/PortToDocs/TestData/DontAddMissingRemarks/DocsExpected.xml diff --git a/Tests/TestData/DontAddMissingRemarks/DocsOriginal.xml b/Tests/PortToDocs/TestData/DontAddMissingRemarks/DocsOriginal.xml similarity index 100% rename from Tests/TestData/DontAddMissingRemarks/DocsOriginal.xml rename to Tests/PortToDocs/TestData/DontAddMissingRemarks/DocsOriginal.xml diff --git a/Tests/TestData/DontAddMissingRemarks/TSOriginal.xml b/Tests/PortToDocs/TestData/DontAddMissingRemarks/TSOriginal.xml similarity index 100% rename from Tests/TestData/DontAddMissingRemarks/TSOriginal.xml rename to Tests/PortToDocs/TestData/DontAddMissingRemarks/TSOriginal.xml diff --git a/Tests/TestData/Exception_ExistingCref/DocsExpected.xml b/Tests/PortToDocs/TestData/Exception_ExistingCref/DocsExpected.xml similarity index 100% rename from Tests/TestData/Exception_ExistingCref/DocsExpected.xml rename to Tests/PortToDocs/TestData/Exception_ExistingCref/DocsExpected.xml diff --git a/Tests/TestData/Exception_ExistingCref/DocsOriginal.xml b/Tests/PortToDocs/TestData/Exception_ExistingCref/DocsOriginal.xml similarity index 100% rename from Tests/TestData/Exception_ExistingCref/DocsOriginal.xml rename to Tests/PortToDocs/TestData/Exception_ExistingCref/DocsOriginal.xml diff --git a/Tests/TestData/Exception_ExistingCref/TSOriginal.xml b/Tests/PortToDocs/TestData/Exception_ExistingCref/TSOriginal.xml similarity index 100% rename from Tests/TestData/Exception_ExistingCref/TSOriginal.xml rename to Tests/PortToDocs/TestData/Exception_ExistingCref/TSOriginal.xml diff --git a/Tests/TestData/Exceptions/DocsExpected.xml b/Tests/PortToDocs/TestData/Exceptions/DocsExpected.xml similarity index 100% rename from Tests/TestData/Exceptions/DocsExpected.xml rename to Tests/PortToDocs/TestData/Exceptions/DocsExpected.xml diff --git a/Tests/TestData/Exceptions/DocsOriginal.xml b/Tests/PortToDocs/TestData/Exceptions/DocsOriginal.xml similarity index 100% rename from Tests/TestData/Exceptions/DocsOriginal.xml rename to Tests/PortToDocs/TestData/Exceptions/DocsOriginal.xml diff --git a/Tests/TestData/Exceptions/TSOriginal.xml b/Tests/PortToDocs/TestData/Exceptions/TSOriginal.xml similarity index 100% rename from Tests/TestData/Exceptions/TSOriginal.xml rename to Tests/PortToDocs/TestData/Exceptions/TSOriginal.xml diff --git a/Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsExpected.xml b/Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsExpected.xml similarity index 100% rename from Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsExpected.xml rename to Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsExpected.xml diff --git a/Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsInterface.xml b/Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsInterface.xml similarity index 100% rename from Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsInterface.xml rename to Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsInterface.xml diff --git a/Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsOriginal.xml b/Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsOriginal.xml similarity index 100% rename from Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsOriginal.xml rename to Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/DocsOriginal.xml diff --git a/Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/TSOriginal.xml b/Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/TSOriginal.xml similarity index 100% rename from Tests/TestData/Remarks_NoEII_NoInterfaceRemarks/TSOriginal.xml rename to Tests/PortToDocs/TestData/Remarks_NoEII_NoInterfaceRemarks/TSOriginal.xml diff --git a/Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsExpected.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsExpected.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsExpected.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsExpected.xml diff --git a/Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsInterface.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsInterface.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsInterface.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsInterface.xml diff --git a/Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsOriginal.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsOriginal.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsOriginal.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/DocsOriginal.xml diff --git a/Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/TSOriginal.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/TSOriginal.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_NoInterfaceRemarks/TSOriginal.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_NoInterfaceRemarks/TSOriginal.xml diff --git a/Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml diff --git a/Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsInterface.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsInterface.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsInterface.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsInterface.xml diff --git a/Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsOriginal.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsOriginal.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsOriginal.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsOriginal.xml diff --git a/Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/TSOriginal.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/TSOriginal.xml similarity index 100% rename from Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/TSOriginal.xml rename to Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/TSOriginal.xml From 43e29bd471d6c10e00780ab594023ee13b1e4d63 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 6 Jan 2021 16:19:05 -0800 Subject: [PATCH 41/65] Move MSBuild registering code into its own method and call it from constructor. --- Libraries/Configuration.cs | 4 +- Libraries/ToTripleSlashPorter.cs | 102 +++++++++++++++++++++++++++++++ Program/DocsPortingTool.cs | 98 ----------------------------- 3 files changed, 104 insertions(+), 100 deletions(-) diff --git a/Libraries/Configuration.cs b/Libraries/Configuration.cs index b39af89..1af406e 100644 --- a/Libraries/Configuration.cs +++ b/Libraries/Configuration.cs @@ -57,8 +57,8 @@ private enum Mode public readonly string BinLogPath = "output.binlog"; public bool BinLogger { get; private set; } = false; - public FileInfo? CsProj { get; private set; } - public PortingDirection Direction { get; private set; } = PortingDirection.ToDocs; + public FileInfo? CsProj { get; set; } + public PortingDirection Direction { get; set; } = PortingDirection.ToDocs; public List DirsIntelliSense { get; } = new List(); public List DirsDocsXml { get; } = new List(); public bool DisablePrompts { get; set; } = false; diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index dfabe2c..1b7e176 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -9,6 +9,10 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Linq; +using Microsoft.Build.Locator; +using System.Collections.Generic; +using System.Runtime.Loader; namespace Libraries { @@ -16,6 +20,7 @@ public class ToTripleSlashPorter { private readonly Configuration Config; private readonly DocsCommentsContainer DocsComments; + private VisualStudioInstance MSBuildInstance; public ToTripleSlashPorter(Configuration config) { @@ -25,6 +30,11 @@ public ToTripleSlashPorter(Configuration config) } Config = config; DocsComments = new DocsCommentsContainer(config); + + // This ensures we can load MSBuild property before calling the ToTripleSlashPorter constructor + MSBuildInstance = MSBuildLocator.QueryVisualStudioInstances().First(); + Register(MSBuildInstance.MSBuildPath); + MSBuildLocator.RegisterInstance(MSBuildInstance); } public void Start() @@ -128,5 +138,97 @@ private void PortCommentsForType(Compilation compilation, IDocsAPI api, ISymbol useBoilerplate = true; } } + + #region MSBuild loading logic + + private static readonly Dictionary s_pathsToAssemblies = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary s_namesToAssemblies = new Dictionary(); + + private static readonly object s_guard = new object(); + + /// + /// Register an assembly loader that will load assemblies with higher version than what was requested. + /// + private static void Register(string searchPath) + { + AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext context, AssemblyName assemblyName) => + { + lock (s_guard) + { + if (s_namesToAssemblies.TryGetValue(assemblyName.FullName, out var cachedAssembly)) + { + return cachedAssembly; + } + + var assembly = TryResolveAssemblyFromPaths(context, assemblyName, searchPath, s_pathsToAssemblies); + + // Cache assembly + if (assembly != null) + { + var name = assembly.FullName; + if (name is null) + { + throw new Exception($"Could not get name for assembly '{assembly}'"); + } + + s_pathsToAssemblies[assembly.Location] = assembly; + s_namesToAssemblies[name] = assembly; + } + + return assembly; + } + }; + } + private static Assembly? TryResolveAssemblyFromPaths(AssemblyLoadContext context, AssemblyName assemblyName, string searchPath, Dictionary? knownAssemblyPaths = null) + { + foreach (var cultureSubfolder in string.IsNullOrEmpty(assemblyName.CultureName) + // If no culture is specified, attempt to load directly from + // the known dependency paths. + ? new[] { string.Empty } + // Search for satellite assemblies in culture subdirectories + // of the assembly search directories, but fall back to the + // bare search directory if that fails. + : new[] { assemblyName.CultureName, string.Empty }) + { + foreach (var extension in new[] { "ni.dll", "ni.exe", "dll", "exe" }) + { + var candidatePath = Path.Combine( + searchPath, cultureSubfolder, $"{assemblyName.Name}.{extension}"); + + var isAssemblyLoaded = knownAssemblyPaths?.ContainsKey(candidatePath) == true; + if (isAssemblyLoaded || !File.Exists(candidatePath)) + { + continue; + } + + var candidateAssemblyName = AssemblyLoadContext.GetAssemblyName(candidatePath); + if (candidateAssemblyName.Version < assemblyName.Version) + { + continue; + } + + try + { + var assembly = context.LoadFromAssemblyPath(candidatePath); + return assembly; + } + catch + { + if (assemblyName.Name != null) + { + // We were unable to load the assembly from the file path. It is likely that + // a different version of the assembly has already been loaded into the context. + // Be forgiving and attempt to load assembly by name without specifying a version. + return context.LoadFromAssemblyName(new AssemblyName(assemblyName.Name)); + } + } + } + } + + return null; + } + + #endregion + } } diff --git a/Program/DocsPortingTool.cs b/Program/DocsPortingTool.cs index 92d1c70..7aae3f8 100644 --- a/Program/DocsPortingTool.cs +++ b/Program/DocsPortingTool.cs @@ -1,12 +1,6 @@ #nullable enable using Libraries; using System; -using System.Collections.Generic; -using System.Reflection; -using System.Runtime.Loader; -using System.IO; -using System.Linq; -using Microsoft.Build.Locator; namespace DocsPortingTool { @@ -25,11 +19,6 @@ public static void Main(string[] args) } case Configuration.PortingDirection.ToTripleSlash: { - // This ensures we can load MSBuild property before calling the ToTripleSlashPorter constructor - VisualStudioInstance? msBuildInstance = MSBuildLocator.QueryVisualStudioInstances().First(); - Register(msBuildInstance.MSBuildPath); - MSBuildLocator.RegisterInstance(msBuildInstance); - var porter = new ToTripleSlashPorter(config); porter.Start(); break; @@ -38,92 +27,5 @@ public static void Main(string[] args) throw new ArgumentOutOfRangeException($"Unrecognized porting direction: {config.Direction}"); } } - - private static readonly Dictionary s_pathsToAssemblies = new Dictionary(StringComparer.OrdinalIgnoreCase); - private static readonly Dictionary s_namesToAssemblies = new Dictionary(); - - private static readonly object s_guard = new object(); - - /// - /// Register an assembly loader that will load assemblies with higher version than what was requested. - /// - private static void Register(string searchPath) - { - AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext context, AssemblyName assemblyName) => - { - lock (s_guard) - { - if (s_namesToAssemblies.TryGetValue(assemblyName.FullName, out var cachedAssembly)) - { - return cachedAssembly; - } - - var assembly = TryResolveAssemblyFromPaths(context, assemblyName, searchPath, s_pathsToAssemblies); - - // Cache assembly - if (assembly != null) - { - var name = assembly.FullName; - if (name is null) - { - throw new Exception($"Could not get name for assembly '{assembly}'"); - } - - s_pathsToAssemblies[assembly.Location] = assembly; - s_namesToAssemblies[name] = assembly; - } - - return assembly; - } - }; - } - internal static Assembly? TryResolveAssemblyFromPaths(AssemblyLoadContext context, AssemblyName assemblyName, string searchPath, Dictionary? knownAssemblyPaths = null) - { - foreach (var cultureSubfolder in string.IsNullOrEmpty(assemblyName.CultureName) - // If no culture is specified, attempt to load directly from - // the known dependency paths. - ? new[] { string.Empty } - // Search for satellite assemblies in culture subdirectories - // of the assembly search directories, but fall back to the - // bare search directory if that fails. - : new[] { assemblyName.CultureName, string.Empty }) - { - foreach (var extension in new[] { "ni.dll", "ni.exe", "dll", "exe" }) - { - var candidatePath = Path.Combine( - searchPath, cultureSubfolder, $"{assemblyName.Name}.{extension}"); - - var isAssemblyLoaded = knownAssemblyPaths?.ContainsKey(candidatePath) == true; - if (isAssemblyLoaded || !File.Exists(candidatePath)) - { - continue; - } - - var candidateAssemblyName = AssemblyLoadContext.GetAssemblyName(candidatePath); - if (candidateAssemblyName.Version < assemblyName.Version) - { - continue; - } - - try - { - var assembly = context.LoadFromAssemblyPath(candidatePath); - return assembly; - } - catch - { - if (assemblyName.Name != null) - { - // We were unable to load the assembly from the file path. It is likely that - // a different version of the assembly has already been loaded into the context. - // Be forgiving and attempt to load assembly by name without specifying a version. - return context.LoadFromAssemblyName(new AssemblyName(assemblyName.Name)); - } - } - } - } - - return null; - } } } From c0b93434bdc5bcf857679b72dc99dce812a53d03 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 6 Jan 2021 16:19:24 -0800 Subject: [PATCH 42/65] Add tests for docs to triple slash porting. --- Tests/PortToDocs/PortToDocsTestData.cs | 73 +++++++++ Tests/PortToDocs/PortToDocsTests.cs | 21 +-- Tests/PortToDocs/TestData.cs | 80 ---------- .../PortToTripleSlashTestData.cs | 67 ++++++++ .../PortToTripleSlashTests.cs | 68 ++++++++ .../TestData/Basic/DocsOriginal.xml | 151 ++++++++++++++++++ .../TestData/Basic/Project.csproj | 9 ++ .../TestData/Basic/SourceExpected.cs | 87 ++++++++++ .../TestData/Basic/SourceOriginal.cs | 34 ++++ Tests/TestData.cs | 22 +++ Tests/Tests.csproj | 14 +- 11 files changed, 535 insertions(+), 91 deletions(-) create mode 100644 Tests/PortToDocs/PortToDocsTestData.cs delete mode 100644 Tests/PortToDocs/TestData.cs create mode 100644 Tests/PortToTripleSlash/PortToTripleSlashTestData.cs create mode 100644 Tests/PortToTripleSlash/PortToTripleSlashTests.cs create mode 100644 Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml create mode 100644 Tests/PortToTripleSlash/TestData/Basic/Project.csproj create mode 100644 Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs create mode 100644 Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs create mode 100644 Tests/TestData.cs diff --git a/Tests/PortToDocs/PortToDocsTestData.cs b/Tests/PortToDocs/PortToDocsTestData.cs new file mode 100644 index 0000000..5fc16c8 --- /dev/null +++ b/Tests/PortToDocs/PortToDocsTestData.cs @@ -0,0 +1,73 @@ +using System.IO; +using Xunit; + +namespace Libraries.Tests +{ + internal class PortToDocsTestData : TestData + { + private const string TestDataRootDirPath = @"../../../PortToDocs/TestData"; + private const string IntellisenseAndDllDirName = "IntelliSenseAndDLL"; + + internal DirectoryInfo IntelliSenseAndDLLDir { get; set; } + + // Docs file with the interface from which the type inherits. + internal string InterfaceFilePath { get; set; } + + internal PortToDocsTestData( + TestDirectory tempDir, + string testDataDir, + string assemblyName, + string namespaceName, + string typeName, + bool skipInterfaceImplementations = true) + { + Assert.False(string.IsNullOrWhiteSpace(assemblyName)); + Assert.False(string.IsNullOrWhiteSpace(typeName)); + + Assembly = assemblyName; + Namespace = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; + Type = typeName; + + IntelliSenseAndDLLDir = tempDir.CreateSubdirectory(IntellisenseAndDllDirName); + DirectoryInfo tripleSlashAssemblyDir = IntelliSenseAndDLLDir.CreateSubdirectory(Assembly); + + DocsDir = tempDir.CreateSubdirectory(DocsDirName); + DirectoryInfo docsAssemblyDir = DocsDir.CreateSubdirectory(Namespace); + + string testDataPath = Path.Combine(TestDataRootDirPath, testDataDir); + + string tripleSlashOriginalFilePath = Path.Combine(testDataPath, "TSOriginal.xml"); + string docsOriginalFilePath = Path.Combine(testDataPath, "DocsOriginal.xml"); + string docsExpectedFilePath = Path.Combine(testDataPath, "DocsExpected.xml"); + + Assert.True(File.Exists(tripleSlashOriginalFilePath)); + Assert.True(File.Exists(docsOriginalFilePath)); + Assert.True(File.Exists(docsExpectedFilePath)); + + OriginalFilePath = Path.Combine(tripleSlashAssemblyDir.FullName, $"{Type}.xml"); + ActualFilePath = Path.Combine(docsAssemblyDir.FullName, $"{Type}.xml"); + ExpectedFilePath = Path.Combine(tempDir.FullPath, "DocsExpected.xml"); + + File.Copy(tripleSlashOriginalFilePath, OriginalFilePath); + File.Copy(docsOriginalFilePath, ActualFilePath); + File.Copy(docsExpectedFilePath, ExpectedFilePath); + + Assert.True(File.Exists(OriginalFilePath)); + Assert.True(File.Exists(ActualFilePath)); + Assert.True(File.Exists(ExpectedFilePath)); + + if (!skipInterfaceImplementations) + { + string interfaceFilePath = Path.Combine(testDataPath, "DocsInterface.xml"); + Assert.True(File.Exists(interfaceFilePath)); + + string interfaceAssembly = "System"; + DirectoryInfo interfaceAssemblyDir = DocsDir.CreateSubdirectory(interfaceAssembly); + InterfaceFilePath = Path.Combine(interfaceAssemblyDir.FullName, "IMyInterface.xml"); + File.Copy(interfaceFilePath, InterfaceFilePath); + Assert.True(File.Exists(InterfaceFilePath)); + } + } + } + +} diff --git a/Tests/PortToDocs/PortToDocsTests.cs b/Tests/PortToDocs/PortToDocsTests.cs index 7e29411..3343128 100644 --- a/Tests/PortToDocs/PortToDocsTests.cs +++ b/Tests/PortToDocs/PortToDocsTests.cs @@ -101,7 +101,7 @@ private void PortToDocs( { using TestDirectory tempDir = new TestDirectory(); - TestData testData = new TestData( + PortToDocsTestData testData = new PortToDocsTestData( tempDir, testDataDir, skipInterfaceImplementations: skipInterfaceImplementations, @@ -110,17 +110,18 @@ private void PortToDocs( typeName: typeName ); - Configuration c = new Configuration + Configuration c = new() { + Direction = Configuration.PortingDirection.ToDocs, DisablePrompts = disablePrompts, + ExceptionCollisionThreshold = exceptionCollisionThreshold, + PortExceptionsExisting = portExceptionsExisting, + PortMemberRemarks = portMemberRemarks, + PortTypeRemarks = portTypeRemarks, PrintUndoc = printUndoc, Save = save, SkipInterfaceImplementations = skipInterfaceImplementations, - SkipInterfaceRemarks = skipInterfaceRemarks, - PortTypeRemarks = portTypeRemarks, - PortMemberRemarks = portMemberRemarks, - PortExceptionsExisting = portExceptionsExisting, - ExceptionCollisionThreshold = exceptionCollisionThreshold + SkipInterfaceRemarks = skipInterfaceRemarks }; c.IncludedAssemblies.Add(assemblyName); @@ -130,8 +131,8 @@ private void PortToDocs( c.IncludedNamespaces.Add(namespaceName); } - c.DirsDocsXml.Add(testData.Docs); - c.DirsIntelliSense.Add(testData.IntelliSenseAndDLL); + c.DirsDocsXml.Add(testData.DocsDir); + c.DirsIntelliSense.Add(testData.IntelliSenseAndDLLDir); var porter = new ToDocsPorter(c); porter.Start(); @@ -139,7 +140,7 @@ private void PortToDocs( Verify(testData); } - private void Verify(TestData testData) + private void Verify(PortToDocsTestData testData) { string[] expectedLines = File.ReadAllLines(testData.ExpectedFilePath); string[] actualLines = File.ReadAllLines(testData.ActualFilePath); diff --git a/Tests/PortToDocs/TestData.cs b/Tests/PortToDocs/TestData.cs deleted file mode 100644 index 391d7de..0000000 --- a/Tests/PortToDocs/TestData.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.IO; -using Xunit; - -namespace Libraries.Tests -{ - public class TestData - { - private string TestDataRootDir => @"..\..\..\PortToDocs\TestData"; - - public const string TestAssembly = "MyAssembly"; - public const string TestNamespace = "MyNamespace"; - public const string TestType = "MyType"; - - public string Assembly { get; private set; } - public string Namespace { get; private set; } - public string Type { get; private set; } - public DirectoryInfo IntelliSenseAndDLL { get; private set; } - public DirectoryInfo Docs { get; private set; } - - /// IntelliSense xml file. - public string OriginalFilePath { get; private set; } - /// Docs file as we should expect it to look. - public string ExpectedFilePath { get; private set; } - /// Docs file the tool will modify. - public string ActualFilePath { get; private set; } - /// Docs file with the interface from which the type inherits. - public string InterfaceFilePath { get; private set; } - - public TestData(TestDirectory tempDir, string testDataDir, string assemblyName, string namespaceName, string typeName, bool skipInterfaceImplementations = true) - { - Assert.False(string.IsNullOrWhiteSpace(assemblyName)); - Assert.False(string.IsNullOrWhiteSpace(typeName)); - - Assembly = assemblyName; - Namespace = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; - Type = typeName; - - IntelliSenseAndDLL = tempDir.CreateSubdirectory("IntelliSenseAndDLL"); - DirectoryInfo tsAssemblyDir = IntelliSenseAndDLL.CreateSubdirectory(Assembly); - - Docs = tempDir.CreateSubdirectory("Docs"); - DirectoryInfo docsAssemblyDir = Docs.CreateSubdirectory(Namespace); - - string testDataPath = Path.Combine(TestDataRootDir, testDataDir); - - string tsOriginFilePath = Path.Combine(testDataPath, "TSOriginal.xml"); - string docsOriginFilePath = Path.Combine(testDataPath, "DocsOriginal.xml"); - string docsOriginExpectedFilePath = Path.Combine(testDataPath, "DocsExpected.xml"); - - Assert.True(File.Exists(tsOriginFilePath)); - Assert.True(File.Exists(docsOriginFilePath)); - Assert.True(File.Exists(docsOriginExpectedFilePath)); - - OriginalFilePath = Path.Combine(tsAssemblyDir.FullName, $"{Type}.xml"); - ActualFilePath = Path.Combine(docsAssemblyDir.FullName, $"{Type}.xml"); - ExpectedFilePath = Path.Combine(tempDir.FullPath, "DocsExpected.xml"); - - File.Copy(tsOriginFilePath, OriginalFilePath); - File.Copy(docsOriginFilePath, ActualFilePath); - File.Copy(docsOriginExpectedFilePath, ExpectedFilePath); - - Assert.True(File.Exists(OriginalFilePath)); - Assert.True(File.Exists(ActualFilePath)); - Assert.True(File.Exists(ExpectedFilePath)); - - if (!skipInterfaceImplementations) - { - string interfaceFilePath = Path.Combine(testDataPath, "DocsInterface.xml"); - Assert.True(File.Exists(interfaceFilePath)); - - string interfaceAssembly = "System"; - DirectoryInfo interfaceAssemblyDir = Docs.CreateSubdirectory(interfaceAssembly); - InterfaceFilePath = Path.Combine(interfaceAssemblyDir.FullName, "IMyInterface.xml"); - File.Copy(interfaceFilePath, InterfaceFilePath); - Assert.True(File.Exists(InterfaceFilePath)); - } - } - } - -} diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs b/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs new file mode 100644 index 0000000..4d3c004 --- /dev/null +++ b/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Libraries.Tests +{ + internal class PortToTripleSlashTestData : TestData + { + private string TestDataRootDirPath => @"../../../PortToTripleSlash/TestData"; + private const string ProjectDirName = "Project"; + private const string ProjectFileName = "Project.csproj"; + private DirectoryInfo ProjectDir { get; set; } + + internal string ProjectFilePath { get; set; } + + internal PortToTripleSlashTestData( + TestDirectory tempDir, + string testDataDir, + string assemblyName, + string namespaceName, + string typeName) + { + Assert.False(string.IsNullOrWhiteSpace(assemblyName)); + Assert.False(string.IsNullOrWhiteSpace(typeName)); + + Assembly = assemblyName; + Namespace = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; + Type = typeName; + + ProjectDir = tempDir.CreateSubdirectory(ProjectDirName); + + DocsDir = tempDir.CreateSubdirectory(DocsDirName); + DirectoryInfo docsAssemblyDir = DocsDir.CreateSubdirectory(Namespace); + + string testDataPath = Path.Combine(TestDataRootDirPath, testDataDir); + + string docsOriginalFilePath = Path.Combine(testDataPath, "DocsOriginal.xml"); + string csOriginalFilePath = Path.Combine(testDataPath, "SourceOriginal.cs"); + string csExpectedFilePath = Path.Combine(testDataPath, "SourceExpected.cs"); + string csprojOriginalFilePath = Path.Combine(testDataPath, "Project.csproj"); + + Assert.True(File.Exists(docsOriginalFilePath)); + Assert.True(File.Exists(csOriginalFilePath)); + Assert.True(File.Exists(csExpectedFilePath)); + Assert.True(File.Exists(csprojOriginalFilePath)); + + OriginalFilePath = Path.Combine(docsAssemblyDir.FullName, $"{Type}.xml"); + ActualFilePath = Path.Combine(ProjectDir.FullName, $"{Type}.cs"); + ExpectedFilePath = Path.Combine(tempDir.FullPath, "SourceExpected.cs"); + ProjectFilePath = Path.Combine(ProjectDir.FullName, ProjectFileName); + + File.Copy(docsOriginalFilePath, OriginalFilePath); + File.Copy(csOriginalFilePath, ActualFilePath); + File.Copy(csExpectedFilePath, ExpectedFilePath); + File.Copy(csprojOriginalFilePath, ProjectFilePath); + + Assert.True(File.Exists(OriginalFilePath)); + Assert.True(File.Exists(ActualFilePath)); + Assert.True(File.Exists(ExpectedFilePath)); + Assert.True(File.Exists(ProjectFilePath)); + } + } +} diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs new file mode 100644 index 0000000..c48c75b --- /dev/null +++ b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs @@ -0,0 +1,68 @@ +using System.IO; +using Xunit; +using Microsoft.Build.Locator; + +namespace Libraries.Tests +{ + public class PortToTripleSlashTests + { + [Fact] + public void Port_Basic() + { + PortToTripleSlash("Basic"); + } + + private void PortToTripleSlash( + string testDataDir, + bool save = true, + string assemblyName = TestData.TestAssembly, + string namespaceName = null, // Most namespaces have the same assembly name + string typeName = TestData.TestType) + { + using TestDirectory tempDir = new TestDirectory(); + + PortToTripleSlashTestData testData = new PortToTripleSlashTestData( + tempDir, + testDataDir, + assemblyName: assemblyName, + namespaceName: namespaceName, + typeName: typeName); + + Configuration c = new() + { + Direction = Configuration.PortingDirection.ToTripleSlash, + CsProj = new FileInfo(testData.ProjectFilePath), + Save = save + }; + + c.IncludedAssemblies.Add(assemblyName); + + if (!string.IsNullOrEmpty(namespaceName)) + { + c.IncludedNamespaces.Add(namespaceName); + } + + c.DirsDocsXml.Add(testData.DocsDir); + + var porter = new ToTripleSlashPorter(c); + porter.Start(); + + Verify(testData); + } + + private void Verify(PortToTripleSlashTestData testData) + { + string[] expectedLines = File.ReadAllLines(testData.ExpectedFilePath); + string[] actualLines = File.ReadAllLines(testData.ActualFilePath); + + for (int i = 0; i < expectedLines.Length; i++) + { + string expectedLine = expectedLines[i]; + string actualLine = actualLines[i]; + Assert.Equal(expectedLine, actualLine); + } + + Assert.Equal(expectedLines.Length, actualLines.Length); + } + } +} diff --git a/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml b/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml new file mode 100644 index 0000000..a01fc01 --- /dev/null +++ b/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml @@ -0,0 +1,151 @@ + + + + MyAssembly + 4.0.0.0 + + + System.Object + + + + This is MyClass summary. + + + + + + + + Property + + + MyAssembly + 4.0.0.0 + + + System.Int32 + + + This is the MyProperty summary. + This is the MyProperty value. + + + + + + + + Field + + MyAssembly + 4.0.0.0 + + + System.Int32 + + 1 + + This is the MyField summary. + + + + + + + + Method + + + MyAssembly + 4.0.0.0 + + + System.Int32 + + + + This is the MyIntMethod param1 summary. + This is MyIntMethod summary. + This is MyIntMethod return value. It mentions the . + + . + + ]]> + + This is the ArgumentNullException thrown by MyIntMethod. It mentions the . + This is the IndexOutOfRangeException thrown by MyIntMethod. + + + + + Method + + + MyAssembly + 4.0.0.0 + + + System.Void + + + + This is MyVoidMethod summary. + + This is MyVoidMethod return value. It mentions the . + + + + . + + ]]> + + + + This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . + + This is the IndexOutOfRangeException thrown by MyVoidMethod. + + + + \ No newline at end of file diff --git a/Tests/PortToTripleSlash/TestData/Basic/Project.csproj b/Tests/PortToTripleSlash/TestData/Basic/Project.csproj new file mode 100644 index 0000000..4d7f14e --- /dev/null +++ b/Tests/PortToTripleSlash/TestData/Basic/Project.csproj @@ -0,0 +1,9 @@ + + + + Library + This is MyNamespace description. + net5.0 + + + diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs new file mode 100644 index 0000000..917d4e8 --- /dev/null +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -0,0 +1,87 @@ +using System; + +namespace MyNamespace +{ + public class MyClass + { + /// This is MyClass summary. + public MyClass() + { + } + + internal MyClass(int myProperty) + { + _myProperty = myProperty; + } + + private int _myProperty; + + /// This is the MyProperty summary. + /// This is the MyProperty value. + /// + public int MyProperty + { + get { return _myProperty; } + set { _myProperty = value; } + } + + /// This is the MyField summary. + /// + public int MyField = 1; + + /// This is MyIntMethod summary. + /// This is MyIntMethod return value. It mentions the . + /// . + /// + /// ]]> + /// This is the ArgumentNullException thrown by MyIntMethod. It mentions the . + /// This is the IndexOutOfRangeException thrown by MyIntMethod. + public int MyIntMethod(int param1) + { + return MyField + param1; + } + + /// This is MyVoidMethod summary. + /// This is MyVoidMethod return value. It mentions the . + /// . + /// + /// ]]> + /// /// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . + /// This is the IndexOutOfRangeException thrown by MyVoidMethod. + public void MyVoidMethod() + { + } + } +} diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs new file mode 100644 index 0000000..9aeec80 --- /dev/null +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs @@ -0,0 +1,34 @@ +using System; + +namespace MyNamespace +{ + public class MyClass + { + public MyClass() + { + } + + internal MyClass(int myProperty) + { + _myProperty = myProperty; + } + + private int _myProperty; + public int MyProperty + { + get { return _myProperty; } + set { _myProperty = value; } + } + + public int MyField = 1; + + public int MyIntMethod(int param1) + { + return MyField + param1; + } + + public void MyVoidMethod() + { + } + } +} diff --git a/Tests/TestData.cs b/Tests/TestData.cs new file mode 100644 index 0000000..ac45c7d --- /dev/null +++ b/Tests/TestData.cs @@ -0,0 +1,22 @@ +using System.IO; + +namespace Libraries.Tests +{ + internal class TestData + { + public const string TestAssembly = "MyAssembly"; + public const string TestNamespace = "MyNamespace"; + public const string TestType = "MyType"; + + protected const string DocsDirName = "Docs"; + + protected string Assembly { get; set; } + protected string Namespace { get; set; } + protected string Type { get; set; } + + internal DirectoryInfo DocsDir { get; set; } + internal string OriginalFilePath { get; set; } + internal string ExpectedFilePath { get; set; } + internal string ActualFilePath { get; set; } + } +} diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 5b5a794..b388247 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -2,10 +2,16 @@ net5.0 - + Microsoft + carlossanlop false + + + + + @@ -24,4 +30,10 @@ + + + + + + From f3689af79f1f2f43f02a5867101eacfe8e9c9928 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 6 Jan 2021 17:53:07 -0800 Subject: [PATCH 43/65] Ensure exceptions are thrown in unit tests when registering MSBuild. Add the Nuget.Frameworks package to the Tests project so that the underlying csproj consumes it without failure. --- Libraries/ToTripleSlashPorter.cs | 19 ++++++++++--------- .../PortToTripleSlashTests.cs | 1 - Tests/Tests.csproj | 1 + 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index 1b7e176..67887fa 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -42,7 +42,7 @@ public void Start() DocsComments.CollectFiles(); if (!DocsComments.Types.Any()) { - Log.ErrorAndExit("No Docs Type APIs found."); + throw new Exception("No Docs Type APIs found."); } Log.Info("Porting from Docs to triple slash..."); @@ -54,8 +54,7 @@ public void Start() } catch (ReflectionTypeLoadException) { - Log.ErrorAndExit("The MSBuild directory was not found in PATH. Use '-MSBuild ' to specify it."); - return; + throw new Exception("The MSBuild directory was not found in PATH. Use '-MSBuild ' to specify it."); } BinaryLogger? binLogger = null; @@ -72,8 +71,7 @@ public void Start() Project? project = workspace.OpenProjectAsync(Config.CsProj!.FullName, msbuildLogger: binLogger).Result; if (project == null) { - Log.ErrorAndExit("Could not find a project."); - return; + throw new Exception("Could not find a project."); } Compilation? compilation = project.GetCompilationAsync().Result; @@ -85,11 +83,14 @@ public void Start() ImmutableList diagnostics = workspace.Diagnostics; if (diagnostics.Any()) { + string allMsgs = Environment.NewLine; foreach (var diagnostic in diagnostics) { - Log.Error($"{diagnostic.Kind} - {diagnostic.Message}"); + string msg = $"{diagnostic.Kind} - {diagnostic.Message}"; + Log.Error(msg); + allMsgs += msg + Environment.NewLine; } - Log.ErrorAndExit("Exiting due to diagnostic errors found."); + throw new Exception("Exiting due to diagnostic errors found: " + allMsgs); } PortCommentsForProject(compilation!); @@ -179,6 +180,7 @@ private static void Register(string searchPath) } }; } + private static Assembly? TryResolveAssemblyFromPaths(AssemblyLoadContext context, AssemblyName assemblyName, string searchPath, Dictionary? knownAssemblyPaths = null) { foreach (var cultureSubfolder in string.IsNullOrEmpty(assemblyName.CultureName) @@ -192,8 +194,7 @@ private static void Register(string searchPath) { foreach (var extension in new[] { "ni.dll", "ni.exe", "dll", "exe" }) { - var candidatePath = Path.Combine( - searchPath, cultureSubfolder, $"{assemblyName.Name}.{extension}"); + var candidatePath = Path.Combine(searchPath, cultureSubfolder, $"{assemblyName.Name}.{extension}"); var isAssemblyLoaded = knownAssemblyPaths?.ContainsKey(candidatePath) == true; if (isAssemblyLoaded || !File.Exists(candidatePath)) diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs index c48c75b..4b5c00e 100644 --- a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs +++ b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs @@ -1,6 +1,5 @@ using System.IO; using Xunit; -using Microsoft.Build.Locator; namespace Libraries.Tests { diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index b388247..ee0ca9b 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -18,6 +18,7 @@ + From ce021d86331b1d16372d98b86faddf32c9ad6378 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 6 Jan 2021 18:53:06 -0800 Subject: [PATCH 44/65] Remove log message that exits, instead throw on error. --- Libraries/Configuration.cs | 46 ++++++++++++++++---------------- Libraries/Log.cs | 14 +++++----- Libraries/ToDocsPorter.cs | 4 +-- Libraries/ToTripleSlashPorter.cs | 2 +- 4 files changed, 32 insertions(+), 34 deletions(-) diff --git a/Libraries/Configuration.cs b/Libraries/Configuration.cs index 1af406e..e097e6e 100644 --- a/Libraries/Configuration.cs +++ b/Libraries/Configuration.cs @@ -100,7 +100,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) if (args == null || args.Length == 0) { - Log.ErrorPrintHelpAndExit("No arguments passed to the executable."); + Log.PrintHelpAndError("No arguments passed to the executable."); } Configuration config = new Configuration(); @@ -120,18 +120,18 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) { if (string.IsNullOrWhiteSpace(arg)) { - Log.ErrorAndExit("You must specify a *.csproj path."); + Log.Error("You must specify a *.csproj path."); } else if (!File.Exists(arg)) { - Log.ErrorAndExit($"The *.csproj file does not exist: {arg}"); + Log.Error($"The *.csproj file does not exist: {arg}"); } else { string ext = Path.GetExtension(arg).ToUpperInvariant(); if (ext != ".CSPROJ") { - Log.ErrorAndExit($"The file does not have a *.csproj extension: {arg}"); + Log.Error($"The file does not have a *.csproj extension: {arg}"); } } config.CsProj = new FileInfo(arg); @@ -157,7 +157,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) config.Direction = PortingDirection.ToTripleSlash; break; default: - Log.ErrorAndExit($"Unrecognized direction value: {arg}"); + Log.Error($"Unrecognized direction value: {arg}"); break; } mode = Mode.Initial; @@ -174,7 +174,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) DirectoryInfo dirInfo = new DirectoryInfo(dirPath); if (!dirInfo.Exists) { - Log.ErrorAndExit($"This Docs xml directory does not exist: {dirPath}"); + Log.Error($"This Docs xml directory does not exist: {dirPath}"); } config.DirsDocsXml.Add(dirInfo); @@ -189,11 +189,11 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) { if (!int.TryParse(arg, out int value)) { - Log.ErrorAndExit($"Invalid int value for 'Exception collision threshold' argument: {arg}"); + Log.Error($"Invalid int value for 'Exception collision threshold' argument: {arg}"); } else if (value < 1 || value > 100) { - Log.ErrorAndExit($"Value needs to be between 0 and 100: {value}"); + Log.Error($"Value needs to be between 0 and 100: {value}"); } config.ExceptionCollisionThreshold = value; @@ -218,7 +218,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) } else { - Log.ErrorPrintHelpAndExit("You must specify at least one assembly."); + Log.PrintHelpAndError("You must specify at least one assembly."); } mode = Mode.Initial; @@ -240,7 +240,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) } else { - Log.ErrorPrintHelpAndExit("You must specify at least one namespace."); + Log.PrintHelpAndError("You must specify at least one namespace."); } mode = Mode.Initial; @@ -262,7 +262,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) } else { - Log.ErrorPrintHelpAndExit("You must specify at least one type name."); + Log.PrintHelpAndError("You must specify at least one type name."); } mode = Mode.Initial; @@ -284,7 +284,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) } else { - Log.ErrorPrintHelpAndExit("You must specify at least one assembly."); + Log.PrintHelpAndError("You must specify at least one assembly."); } mode = Mode.Initial; @@ -306,7 +306,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) } else { - Log.ErrorPrintHelpAndExit("You must specify at least one namespace."); + Log.PrintHelpAndError("You must specify at least one namespace."); } mode = Mode.Initial; @@ -328,7 +328,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) } else { - Log.ErrorPrintHelpAndExit("You must specify at least one type name."); + Log.PrintHelpAndError("You must specify at least one type name."); } mode = Mode.Initial; @@ -462,7 +462,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) break; default: - Log.ErrorPrintHelpAndExit($"Unrecognized argument: {arg}"); + Log.PrintHelpAndError($"Unrecognized argument: {arg}"); break; } break; @@ -478,7 +478,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) DirectoryInfo dirInfo = new DirectoryInfo(dirPath); if (!dirInfo.Exists) { - Log.ErrorAndExit($"This IntelliSense directory does not exist: {dirPath}"); + Log.Error($"This IntelliSense directory does not exist: {dirPath}"); } config.DirsIntelliSense.Add(dirInfo); @@ -603,7 +603,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) default: { - Log.ErrorPrintHelpAndExit("Unexpected mode."); + Log.PrintHelpAndError("Unexpected mode."); break; } } @@ -611,19 +611,19 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) if (mode != Mode.Initial) { - Log.ErrorPrintHelpAndExit("You missed an argument value."); + Log.PrintHelpAndError("You missed an argument value."); } if (config.DirsDocsXml == null) { - Log.ErrorPrintHelpAndExit($"You must specify a path to the dotnet-api-docs xml folder using '-{nameof(Mode.Docs)}'."); + Log.PrintHelpAndError($"You must specify a path to the dotnet-api-docs xml folder using '-{nameof(Mode.Docs)}'."); } if (config.Direction == PortingDirection.ToDocs) { if (config.DirsIntelliSense.Count == 0) { - Log.ErrorPrintHelpAndExit($"You must specify at least one IntelliSense & DLL folder using '-{nameof(Mode.IntelliSense)}'."); + Log.PrintHelpAndError($"You must specify at least one IntelliSense & DLL folder using '-{nameof(Mode.IntelliSense)}'."); } } @@ -631,13 +631,13 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) { if (config.CsProj == null) { - Log.ErrorPrintHelpAndExit($"You must specify a *.csproj file using '-{nameof(Mode.CsProj)}'."); + Log.PrintHelpAndError($"You must specify a *.csproj file using '-{nameof(Mode.CsProj)}'."); } } if (config.IncludedAssemblies.Count == 0) { - Log.ErrorPrintHelpAndExit($"You must specify at least one assembly with {nameof(IncludedAssemblies)}."); + Log.PrintHelpAndError($"You must specify at least one assembly with {nameof(IncludedAssemblies)}."); } return config; @@ -648,7 +648,7 @@ private static bool ParseOrExit(string arg, string paramFriendlyName) { if (!bool.TryParse(arg, out bool value)) { - Log.ErrorAndExit($"Invalid boolean value for '{paramFriendlyName}' argument: {arg}"); + Log.Error($"Invalid boolean value for '{paramFriendlyName}' argument: {arg}"); } Log.Cyan($"{paramFriendlyName}:"); diff --git a/Libraries/Log.cs b/Libraries/Log.cs index 5866fe6..822e7cf 100644 --- a/Libraries/Log.cs +++ b/Libraries/Log.cs @@ -102,6 +102,11 @@ public static void Error(string format, params object[]? args) public static void Error(bool endline, string format, params object[]? args) { Print(endline, ConsoleColor.Red, format, args); + + if (args == null) + throw new Exception(format); + else + throw new Exception(string.Format(format, args)); } public static void Cyan(string format) @@ -158,17 +163,10 @@ public static void Line() public delegate void PrintHelpFunction(); - public static void ErrorAndExit(string format, params object[]? args) - { - Error(format, args); - Environment.Exit(0); - } - - public static void ErrorPrintHelpAndExit(string format, params object[]? args) + public static void PrintHelpAndError(string format, params object[]? args) { PrintHelp(); Error(format, args); - Environment.Exit(0); } public static void PrintHelp() diff --git a/Libraries/ToDocsPorter.cs b/Libraries/ToDocsPorter.cs index 38b2d10..a3f87d1 100644 --- a/Libraries/ToDocsPorter.cs +++ b/Libraries/ToDocsPorter.cs @@ -40,13 +40,13 @@ public void Start() if (!IntelliSenseXmlComments.Members.Any()) { - Log.ErrorAndExit("No IntelliSense xml comments found."); + Log.Error("No IntelliSense xml comments found."); } DocsComments.CollectFiles(); if (!DocsComments.Types.Any()) { - Log.ErrorAndExit("No Docs Type APIs found."); + Log.Error("No Docs Type APIs found."); } PortMissingComments(); diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index 67887fa..81edf02 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -42,7 +42,7 @@ public void Start() DocsComments.CollectFiles(); if (!DocsComments.Types.Any()) { - throw new Exception("No Docs Type APIs found."); + Log.Error("No Docs Type APIs found."); } Log.Info("Porting from Docs to triple slash..."); From 9537939bba4292e9e92a99c84f9c5093c97b3f73 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 6 Jan 2021 18:53:58 -0800 Subject: [PATCH 45/65] Fix minor errors in test files. --- .../TestData/Basic/DocsOriginal.xml | 55 +++++++++++-------- .../TestData/Basic/SourceExpected.cs | 38 ++++++++----- .../TestData/Basic/SourceOriginal.cs | 7 ++- 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml b/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml index a01fc01..3343dbc 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml +++ b/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml @@ -1,5 +1,5 @@ - - + + MyAssembly 4.0.0.0 @@ -9,22 +9,35 @@ - This is MyClass summary. + This is MyType class summary. + ]]> + + + Constructor + + MyAssembly + 4.0.0.0 + + + + This is the MyType constructor summary. + To be added. + + - + Property @@ -38,7 +51,7 @@ Multiple lines. This is the MyProperty summary. This is the MyProperty value. - - + - + Field MyAssembly @@ -77,7 +90,7 @@ Multiple lines. - + Method @@ -90,8 +103,8 @@ Multiple lines. This is the MyIntMethod param1 summary. - This is MyIntMethod summary. - This is MyIntMethod return value. It mentions the . + This is the MyIntMethod summary. + This is the MyIntMethod return value. It mentions the . . - + Method @@ -122,13 +135,10 @@ Mentions the `param1` and the . - This is MyVoidMethod summary. - - This is MyVoidMethod return value. It mentions the . - + This is the MyVoidMethod summary. + This is the MyVoidMethod return value. It mentions the . - - . - ]]> - + ]]> - - This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . - + This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . This is the IndexOutOfRangeException thrown by MyVoidMethod. diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index 917d4e8..ea118a8 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -2,14 +2,24 @@ namespace MyNamespace { - public class MyClass + /// This is MyType class summary. + /// + public class MyType { - /// This is MyClass summary. - public MyClass() + /// This is the MyType constructor summary. + public MyType() { } - internal MyClass(int myProperty) + internal MyType(int myProperty) { _myProperty = myProperty; } @@ -20,11 +30,11 @@ internal MyClass(int myProperty) /// This is the MyProperty value. /// public int MyProperty @@ -45,8 +55,8 @@ public int MyProperty /// ]]> public int MyField = 1; - /// This is MyIntMethod summary. - /// This is MyIntMethod return value. It mentions the . + /// This is the MyIntMethod summary. + /// This is the MyIntMethod return value. It mentions the . /// /// This is the ArgumentNullException thrown by MyIntMethod. It mentions the . - /// This is the IndexOutOfRangeException thrown by MyIntMethod. + /// This is the IndexOutOfRangeException thrown by MyIntMethod. public int MyIntMethod(int param1) { return MyField + param1; } - /// This is MyVoidMethod summary. - /// This is MyVoidMethod return value. It mentions the . + /// This is the MyVoidMethod summary. + /// This is the MyVoidMethod return value. It mentions the . /// . /// /// ]]> - /// /// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . - /// This is the IndexOutOfRangeException thrown by MyVoidMethod. + /// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . + /// This is the IndexOutOfRangeException thrown by MyVoidMethod. public void MyVoidMethod() { } diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs index 9aeec80..d47c025 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs @@ -2,18 +2,19 @@ namespace MyNamespace { - public class MyClass + public class MyType { - public MyClass() + public MyType() { } - internal MyClass(int myProperty) + internal MyType(int myProperty) { _myProperty = myProperty; } private int _myProperty; + public int MyProperty { get { return _myProperty; } From 5861c1dab90bbfc5312f8fb7797a8986fdb28673 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 6 Jan 2021 18:54:16 -0800 Subject: [PATCH 46/65] Fix assembly and namespace naming in test code. --- Tests/PortToTripleSlash/PortToTripleSlashTestData.cs | 7 +------ Tests/PortToTripleSlash/PortToTripleSlashTests.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs b/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs index 4d3c004..42a4835 100644 --- a/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs +++ b/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Xunit; namespace Libraries.Tests @@ -12,9 +9,7 @@ internal class PortToTripleSlashTestData : TestData { private string TestDataRootDirPath => @"../../../PortToTripleSlash/TestData"; private const string ProjectDirName = "Project"; - private const string ProjectFileName = "Project.csproj"; private DirectoryInfo ProjectDir { get; set; } - internal string ProjectFilePath { get; set; } internal PortToTripleSlashTestData( @@ -51,7 +46,7 @@ internal PortToTripleSlashTestData( OriginalFilePath = Path.Combine(docsAssemblyDir.FullName, $"{Type}.xml"); ActualFilePath = Path.Combine(ProjectDir.FullName, $"{Type}.cs"); ExpectedFilePath = Path.Combine(tempDir.FullPath, "SourceExpected.cs"); - ProjectFilePath = Path.Combine(ProjectDir.FullName, ProjectFileName); + ProjectFilePath = Path.Combine(ProjectDir.FullName, $"{Assembly}.csproj"); File.Copy(docsOriginalFilePath, OriginalFilePath); File.Copy(csOriginalFilePath, ActualFilePath); diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs index 4b5c00e..d1a9554 100644 --- a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs +++ b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs @@ -15,7 +15,7 @@ private void PortToTripleSlash( string testDataDir, bool save = true, string assemblyName = TestData.TestAssembly, - string namespaceName = null, // Most namespaces have the same assembly name + string namespaceName = TestData.TestNamespace, string typeName = TestData.TestType) { using TestDirectory tempDir = new TestDirectory(); @@ -58,6 +58,13 @@ private void Verify(PortToTripleSlashTestData testData) { string expectedLine = expectedLines[i]; string actualLine = actualLines[i]; + if (System.Diagnostics.Debugger.IsAttached) + { + if (expectedLine != actualLine) + { + System.Diagnostics.Debugger.Break(); + } + } Assert.Equal(expectedLine, actualLine); } From 7f6c6b1f36d46b23bbfa791c96e1528f82e9f283 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Wed, 6 Jan 2021 22:56:27 -0800 Subject: [PATCH 47/65] Fine tuning detection of some elements. --- Libraries/Extensions.cs | 9 + .../TripleSlashSyntaxRewriter.cs | 154 +++++++++++------- .../TestData/Basic/DocsOriginal.xml | 38 +++-- .../TestData/Basic/SourceExpected.cs | 61 ++++--- .../TestData/Basic/SourceOriginal.cs | 8 +- 5 files changed, 163 insertions(+), 107 deletions(-) diff --git a/Libraries/Extensions.cs b/Libraries/Extensions.cs index 8f92ab7..397b5e5 100644 --- a/Libraries/Extensions.cs +++ b/Libraries/Extensions.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Generic; +using System.Text.RegularExpressions; namespace Libraries { @@ -37,6 +38,14 @@ public static string RemoveSubstrings(this string oldString, params string[] str // Checks if the passed string is considered "empty" according to the Docs repo rules. public static bool IsDocsEmpty(this string? s) => string.IsNullOrWhiteSpace(s) || s == Configuration.ToBeAdded; + + public static string WithoutPrefixes(this string text) + { + return Regex.Replace( + input: text, + pattern: @"(?.*)(?cref=""[A-Z]\:)(?.*)", + replacement: "${left}cref=\"${right}"); + } } } diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index 8432ee4..f80a0d2 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -25,11 +25,6 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod UseBoilerplate = useBoilerplate; } - /// - /// - /// - /// - /// public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) { SyntaxNode? baseNode = base.VisitClassDeclaration(node); @@ -56,8 +51,29 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod public override SyntaxNode? VisitEventDeclaration(EventDeclarationSyntax node) => VisitMemberDeclaration(node); - public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node) => - VisitMemberDeclaration(node); + public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax 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 summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + + return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks); + } + + return node; + } public override SyntaxNode? VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) { @@ -100,7 +116,7 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); - return GetNodeWithTrivia(node, summary, value, remarks, exceptions, seealsos); + return GetNodeWithTrivia(leadingWhitespace, node, summary, value, remarks, exceptions, seealsos); } public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node) @@ -150,23 +166,35 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList summary = GetSummary(summaryText, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(remarksText, leadingWhitespace); - return GetNodeWithTrivia(node, summary, remarks); + return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks); } - private SyntaxNode GetNodeWithTrivia(SyntaxNode node, params SyntaxTriviaList[] trivias) + private SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxNode node, params SyntaxTriviaList[] trivias) { - SyntaxTriviaList finalTrivia = new(SyntaxFactory.CarriageReturnLineFeed); // Space to separate from previous definition + SyntaxTriviaList finalTrivia = new(); + var leadingTrivia = node.GetLeadingTrivia(); + if (leadingTrivia.Any()) + { + if (leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) + { + // Ensure the endline that separates nodes is respected + finalTrivia = new(SyntaxFactory.ElasticCarriageReturnLineFeed); + } + } + foreach (SyntaxTriviaList t in trivias) { finalTrivia = finalTrivia.AddRange(t); } - finalTrivia = finalTrivia.AddRange(GetLeadingWhitespace(node)); // spaces before type declaration + finalTrivia = finalTrivia.AddRange(leadingWhitespace); return node.WithLeadingTrivia(finalTrivia); } 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; @@ -175,27 +203,14 @@ private SyntaxNode GetNodeWithTrivia(SyntaxNode node, params SyntaxTriviaList[] SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); - - SyntaxTriviaList parameters = new(); - foreach (SyntaxTriviaList parameterTrivia in member.Params.Select( - param => GetParam(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) - { - parameters = parameters.AddRange(parameterTrivia); - } - - SyntaxTriviaList typeParameters = new(); - foreach (SyntaxTriviaList typeParameterTrivia in member.TypeParams.Select( - param => GetTypeParam(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) - { - typeParameters = typeParameters.AddRange(typeParameterTrivia); - } - + SyntaxTriviaList parameters = GetParameters(member, leadingWhitespace); + SyntaxTriviaList typeParameters = GetTypeParameters(member, leadingWhitespace); SyntaxTriviaList returns = GetReturns(UseBoilerplate ? BoilerplateText : member.Returns, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); - return GetNodeWithTrivia(node, summary, parameters, typeParameters, returns, remarks, exceptions, seealsos); + return GetNodeWithTrivia(leadingWhitespace, node, summary, parameters, typeParameters, returns, remarks, exceptions, seealsos); } private SyntaxNode? VisitMemberDeclaration(MemberDeclarationSyntax node) @@ -209,19 +224,9 @@ private SyntaxNode GetNodeWithTrivia(SyntaxNode node, params SyntaxTriviaList[] SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); - SyntaxTriviaList exceptions = new(); - // No need to add exceptions in secondary files - if (!UseBoilerplate && member.Exceptions.Any()) - { - foreach (SyntaxTriviaList exceptionsTrivia in member.Exceptions.Select( - exception => GetException(exception.Cref, exception.Value, leadingWhitespace))) - { - exceptions = exceptions.AddRange(exceptionsTrivia); - } - } - - return GetNodeWithTrivia(node, summary, remarks, exceptions); + return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks, exceptions); } private SyntaxTriviaList GetLeadingWhitespace(SyntaxNode node) => @@ -229,7 +234,7 @@ private SyntaxTriviaList GetLeadingWhitespace(SyntaxNode node) => private SyntaxTriviaList GetSummary(string text, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text); + SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); XmlElementSyntax element = SyntaxFactory.XmlSummaryElement(contents); return GetXmlTrivia(element, leadingWhitespace); } @@ -238,7 +243,7 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp { if (!UseBoilerplate && !text.IsDocsEmpty()) { - string trimmedRemarks = text.RemoveSubstrings("").Trim(); // The SyntaxFactory needs to add this + string trimmedRemarks = text.RemoveSubstrings("").Trim(); // The SyntaxFactory needs to be the one to add these SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true); XmlNodeSyntax xmlRemarksContent = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(xmlRemarksContent); @@ -251,25 +256,47 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp private SyntaxTriviaList GetValue(string text, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text); + SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); XmlElementSyntax element = SyntaxFactory.XmlValueElement(contents); return GetXmlTrivia(element, leadingWhitespace); } private SyntaxTriviaList GetParam(string name, string text, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text); + SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); XmlElementSyntax element = SyntaxFactory.XmlParamElement(name, contents); return GetXmlTrivia(element, leadingWhitespace); } private SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace) { - var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute(name, text)); + var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute("name", name)); SyntaxList contents = GetContentsInRows(text); return GetXmlTrivia("typeparam", attribute, contents, leadingWhitespace); } + private SyntaxTriviaList GetParameters(DocsMember member, SyntaxTriviaList leadingWhitespace) + { + SyntaxTriviaList parameters = new(); + foreach (SyntaxTriviaList parameterTrivia in member.Params.Select( + param => GetParam(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) + { + parameters = parameters.AddRange(parameterTrivia); + } + return parameters; + } + + private SyntaxTriviaList GetTypeParameters(DocsMember member, SyntaxTriviaList leadingWhitespace) + { + SyntaxTriviaList typeParameters = new(); + foreach (SyntaxTriviaList typeParameterTrivia in member.TypeParams.Select( + typeParam => GetTypeParam(typeParam.Name, UseBoilerplate ? BoilerplateText : typeParam.Value, leadingWhitespace))) + { + typeParameters = typeParameters.AddRange(typeParameterTrivia); + } + return typeParameters; + } + private SyntaxTriviaList GetReturns(string text, SyntaxTriviaList leadingWhitespace) { // For when returns is empty because the method returns void @@ -278,11 +305,24 @@ private SyntaxTriviaList GetReturns(string text, SyntaxTriviaList leadingWhitesp return new(); } - SyntaxList contents = GetContentsInRows(text); + SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); XmlElementSyntax element = SyntaxFactory.XmlReturnsElement(contents); return GetXmlTrivia(element, leadingWhitespace); } + private SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace) + { + if (cref.Length > 2 && cref[1] == ':') + { + cref = cref[2..]; + } + + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); + XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text.WithoutPrefixes(), leadingWhitespace, addInitialNewLine: false)); + XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); + return GetXmlTrivia(element, leadingWhitespace); + } + private SyntaxTriviaList GetExceptions(DocsMember member, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList exceptions = new(); @@ -298,14 +338,6 @@ private SyntaxTriviaList GetExceptions(DocsMember member, SyntaxTriviaList leadi return exceptions; } - private SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace) - { - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); - XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text, leadingWhitespace, addInitialNewLine: false)); - XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); - return GetXmlTrivia(element, leadingWhitespace); - } - private SyntaxTriviaList GetSeeAlsos(DocsMember member, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList seealsos = new(); @@ -323,6 +355,11 @@ private SyntaxTriviaList GetSeeAlsos(DocsMember member, SyntaxTriviaList leading private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace) { + if (cref.Length > 2 && cref[1] == ':') + { + cref = cref[2..]; + } + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); XmlEmptyElementSyntax element = SyntaxFactory.XmlSeeAlsoElement(crefSyntax); return GetXmlTrivia(element, leadingWhitespace); @@ -358,13 +395,7 @@ private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhi if (splittedLines.Length > 1) { tokens.Add(newLineAndWhitespace); - - if (lineNumber < splittedLines.Length) - { - // New line characters between sentences need to have their own separate line - // but need to avoid adding a final single separate line - tokens.Add(newLineAndWhitespace); - } + tokens.Add(newLineAndWhitespace); } lineNumber++; @@ -426,6 +457,7 @@ private bool TryGetMember(SyntaxNode node, [NotNullWhen(returnValue: true)] out member = DocsComments.Members.FirstOrDefault(m => m.DocId == docId); } } + return member != null; } diff --git a/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml b/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml index 3343dbc..b0e4026 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml +++ b/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml @@ -2,14 +2,9 @@ MyAssembly - 4.0.0.0 - - System.Object - - - This is MyType class summary. + This is the MyType class summary. - + Constructor MyAssembly - 4.0.0.0 - This is the MyType constructor summary. To be added. - + Property MyAssembly - 4.0.0.0 System.Int32 @@ -68,7 +60,6 @@ Multiple lines. Field MyAssembly - 4.0.0.0 System.Int32 @@ -90,19 +81,18 @@ Multiple lines. - + Method MyAssembly - 4.0.0.0 System.Int32 - This is the MyIntMethod param1 summary. + This is the MyIntMethod param2 summary. This is the MyIntMethod summary. This is the MyIntMethod return value. It mentions the . @@ -133,10 +123,8 @@ Mentions the `param1` and the . System.Void - This is the MyVoidMethod summary. - This is the MyVoidMethod return value. It mentions the . . This is the IndexOutOfRangeException thrown by MyVoidMethod. + + + Method + + + MyAssembly + + + System.Void + + + This is the MyTypeParamMethod typeparam T. + This is the MyTypeParamMethod summary. + To be added. + + \ No newline at end of file diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index ea118a8..02fde4d 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -2,15 +2,15 @@ namespace MyNamespace { - /// This is MyType class summary. + /// This is the MyType class summary. /// public class MyType { @@ -29,13 +29,13 @@ internal MyType(int myProperty) /// This is the MyProperty summary. /// This is the MyProperty value. /// public int MyProperty { @@ -45,53 +45,60 @@ public int MyProperty /// This is the MyField summary. /// public int MyField = 1; /// 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 . /// . - /// + /// /// ]]> /// This is the ArgumentNullException thrown by MyIntMethod. It mentions the . /// This is the IndexOutOfRangeException thrown by MyIntMethod. - public int MyIntMethod(int param1) + public int MyIntMethod(int param1, int param2) { - return MyField + param1; + return MyField + param1 + param2; } /// This is the MyVoidMethod summary. - /// This is the MyVoidMethod return value. It mentions the . /// . - /// + /// + /// Mentions the . + /// /// ]]> /// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . /// This is the IndexOutOfRangeException thrown by MyVoidMethod. public void MyVoidMethod() { } + + /// This is the MyTypeParamMethod summary. + /// This is the MyTypeParamMethod typeparam T. + public void MyTypeParamMethod() + { + } } } diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs index d47c025..f5b59f2 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs @@ -23,13 +23,17 @@ public int MyProperty public int MyField = 1; - public int MyIntMethod(int param1) + public int MyIntMethod(int param1, int param2) { - return MyField + param1; + return MyField + param1 + param2; } public void MyVoidMethod() { } + + public void MyTypeParamMethod() + { + } } } From 8469636e657d733e6d2125116bd82269a884dbc7 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 7 Jan 2021 01:08:05 -0800 Subject: [PATCH 48/65] Support altmember, related and seealso. Support event, field and delegate. --- Libraries/Docs/DocsAPI.cs | 52 +++- Libraries/Docs/DocsMember.cs | 20 -- Libraries/Docs/DocsRelated.cs | 39 +++ Libraries/Docs/DocsSeeAlso.cs | 34 --- Libraries/Extensions.cs | 7 +- .../TripleSlashSyntaxRewriter.cs | 254 ++++++++++++------ Tests/PortToDocs/PortToDocsTestData.cs | 19 +- .../PortToTripleSlashTestData.cs | 52 ++-- .../{Project.csproj => MyAssembly.csproj} | 0 .../TestData/Basic/MyDelegate.xml | 22 ++ .../Basic/{DocsOriginal.xml => MyType.xml} | 13 + .../TestData/Basic/SourceExpected.cs | 11 + .../TestData/Basic/SourceOriginal.cs | 4 + Tests/TestData.cs | 17 +- Tests/Tests.csproj | 2 +- 15 files changed, 344 insertions(+), 202 deletions(-) create mode 100644 Libraries/Docs/DocsRelated.cs delete mode 100644 Libraries/Docs/DocsSeeAlso.cs rename Tests/PortToTripleSlash/TestData/Basic/{Project.csproj => MyAssembly.csproj} (100%) create mode 100644 Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml rename Tests/PortToTripleSlash/TestData/Basic/{DocsOriginal.xml => MyType.xml} (92%) diff --git a/Libraries/Docs/DocsAPI.cs b/Libraries/Docs/DocsAPI.cs index 86eead0..64b2a32 100644 --- a/Libraries/Docs/DocsAPI.cs +++ b/Libraries/Docs/DocsAPI.cs @@ -14,7 +14,9 @@ internal abstract class DocsAPI : IDocsAPI private List? _typeParameters; private List? _typeParams; private List? _assemblyInfos; - private List? _seeAlsos; + private List? _seeAlsoCrefs; + private List? _altMemberCrefs; + private List? _relateds; protected readonly XElement XERoot; @@ -122,22 +124,60 @@ public List TypeParams } } - public List SeeAlsos + public List SeeAlsoCrefs { get { - if (_seeAlsos == null) + if (_seeAlsoCrefs == null) { if (Docs != null) { - _seeAlsos = Docs.Elements("seealso").Select(x => new DocsSeeAlso(this, x)).ToList(); + _seeAlsoCrefs = Docs.Elements("seealso").Select(x => XmlHelper.GetAttributeValue(x, "cref")).ToList(); } else { - _seeAlsos = new(); + _seeAlsoCrefs = new(); } } - return _seeAlsos; + return _seeAlsoCrefs; + } + } + + public List AltMembers + { + get + { + if (_altMemberCrefs == null) + { + if (Docs != null) + { + _altMemberCrefs = Docs.Elements("altmember").Select(x => XmlHelper.GetAttributeValue(x, "cref")).ToList(); + } + else + { + _altMemberCrefs = new(); + } + } + return _altMemberCrefs; + } + } + + 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; } } diff --git a/Libraries/Docs/DocsMember.cs b/Libraries/Docs/DocsMember.cs index 1383004..75e5a0a 100644 --- a/Libraries/Docs/DocsMember.cs +++ b/Libraries/Docs/DocsMember.cs @@ -10,7 +10,6 @@ internal class DocsMember : DocsAPI private string? _memberName; private List? _memberSignatures; private string? _docId; - private List? _altMemberCref; private List? _exceptions; public DocsMember(string filePath, DocsType parentType, XElement xeMember) @@ -169,25 +168,6 @@ public string Value } } - public List AltMemberCref - { - get - { - if (_altMemberCref == null) - { - if (Docs != null) - { - _altMemberCref = Docs.Elements("altmember").Select(x => XmlHelper.GetAttributeValue(x, "cref")).ToList(); - } - else - { - _altMemberCref = new List(); - } - } - return _altMemberCref; - } - } - public List Exceptions { get diff --git a/Libraries/Docs/DocsRelated.cs b/Libraries/Docs/DocsRelated.cs new file mode 100644 index 0000000..360b1f6 --- /dev/null +++ b/Libraries/Docs/DocsRelated.cs @@ -0,0 +1,39 @@ +using System.Xml.Linq; + +namespace Libraries.Docs +{ + internal class DocsRelated + { + private readonly XElement XERelatedArticle; + + public IDocsAPI ParentAPI + { + get; private set; + } + + 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 DocsRelated(IDocsAPI parentAPI, XElement xeRelatedArticle) + { + ParentAPI = parentAPI; + XERelatedArticle = xeRelatedArticle; + } + + public override string ToString() + { + return Value; + } + } +} diff --git a/Libraries/Docs/DocsSeeAlso.cs b/Libraries/Docs/DocsSeeAlso.cs deleted file mode 100644 index ca218b8..0000000 --- a/Libraries/Docs/DocsSeeAlso.cs +++ /dev/null @@ -1,34 +0,0 @@ -#nullable enable -using System.Xml.Linq; - -namespace Libraries.Docs -{ - internal class DocsSeeAlso - { - private readonly XElement XESeeAlso; - - public IDocsAPI ParentAPI - { - get; private set; - } - - public string Cref - { - get - { - return XmlHelper.GetAttributeValue(XESeeAlso, "cref"); - } - } - - public DocsSeeAlso(IDocsAPI parentAPI, XElement xeSeeAlso) - { - ParentAPI = parentAPI; - XESeeAlso = xeSeeAlso; - } - - public override string ToString() - { - return $"{Cref}"; - } - } -} diff --git a/Libraries/Extensions.cs b/Libraries/Extensions.cs index 397b5e5..56c6a7b 100644 --- a/Libraries/Extensions.cs +++ b/Libraries/Extensions.cs @@ -39,8 +39,13 @@ public static string RemoveSubstrings(this string oldString, params string[] str public static bool IsDocsEmpty(this string? s) => string.IsNullOrWhiteSpace(s) || s == Configuration.ToBeAdded; - public static string WithoutPrefixes(this string text) + public static string WithoutPrefix(this string text) { + if (text.Length > 2 && text[1] == ':') + { + return text[2..]; + } + return Regex.Replace( input: text, pattern: @"(?.*)(?cref=""[A-Z]\:)(?.*)", diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index f80a0d2..fe166f8 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -25,6 +25,8 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod UseBoilerplate = useBoilerplate; } + #region Visitor overrides + public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) { SyntaxNode? baseNode = base.VisitClassDeclaration(node); @@ -42,38 +44,31 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod public override SyntaxNode? VisitConstructorDeclaration(ConstructorDeclarationSyntax node) => VisitBaseMethodDeclaration(node); - public override SyntaxNode? VisitEnumDeclaration(EnumDeclarationSyntax node) => - VisitMemberDeclaration(node); - - public override SyntaxNode? VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) => - VisitMemberDeclaration(node); - - public override SyntaxNode? VisitEventDeclaration(EventDeclarationSyntax node) => - VisitMemberDeclaration(node); - - public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node) + public override SyntaxNode? VisitDelegateDeclaration(DelegateDeclarationSyntax node) { - // The comments need to be extracted from the underlying variable declarator inside the declaration - VariableDeclarationSyntax declaration = node.Declaration; + SyntaxNode? baseNode = base.VisitDelegateDeclaration(node); - // Only port docs if there is only one variable in the declaration - if (declaration.Variables.Count == 1) + ISymbol? symbol = Model.GetDeclaredSymbol(node); + if (symbol == null) { - if (!TryGetMember(declaration.Variables.First(), out DocsMember? member)) - { - return node; - } + Log.Warning($"Symbol is null."); + return baseNode; + } - SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); + return VisitType(baseNode, symbol); + } - SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + public override SyntaxNode? VisitEnumDeclaration(EnumDeclarationSyntax node) => + VisitMemberDeclaration(node); - return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks); - } + public override SyntaxNode? VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) => + VisitMemberDeclaration(node); - return node; - } + public override SyntaxNode? VisitEventFieldDeclaration(EventFieldDeclarationSyntax node) => + VisitVariableDeclaration(node); + + public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node) => + VisitVariableDeclaration(node); public override SyntaxNode? VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) { @@ -115,8 +110,10 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); - return GetNodeWithTrivia(leadingWhitespace, node, summary, value, remarks, exceptions, seealsos); + return GetNodeWithTrivia(leadingWhitespace, node, summary, value, remarks, exceptions, seealsos, altmembers, relateds); } public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node) @@ -133,6 +130,8 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod return VisitType(baseNode, symbol); } + #endregion + private SyntaxNode? VisitType(SyntaxNode? node, ISymbol? symbol) { if (node == null || symbol == null) @@ -150,6 +149,14 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod string summaryText = BoilerplateText; string remarksText = string.Empty; + SyntaxTriviaList parameters = new(); + SyntaxTriviaList typeParameters = new(); + SyntaxTriviaList seealsos = new(); + SyntaxTriviaList altmembers = new(); + SyntaxTriviaList relateds = new(); + + SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); + if (!UseBoilerplate) { if (!TryGetType(symbol, out DocsType? type)) @@ -159,36 +166,18 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod summaryText = type.Summary; remarksText = type.Remarks; - } - SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); + parameters = GetParameters(type, leadingWhitespace); + typeParameters = GetTypeParameters(type, leadingWhitespace); + seealsos = GetSeeAlsos(type, leadingWhitespace); + altmembers = GetAltMembers(type, leadingWhitespace); + relateds = GetRelateds(type, leadingWhitespace); + } SyntaxTriviaList summary = GetSummary(summaryText, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(remarksText, leadingWhitespace); - return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks); - } - - private SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxNode node, params SyntaxTriviaList[] trivias) - { - SyntaxTriviaList finalTrivia = new(); - var leadingTrivia = node.GetLeadingTrivia(); - if (leadingTrivia.Any()) - { - if (leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) - { - // Ensure the endline that separates nodes is respected - finalTrivia = new(SyntaxFactory.ElasticCarriageReturnLineFeed); - } - } - - foreach (SyntaxTriviaList t in trivias) - { - finalTrivia = finalTrivia.AddRange(t); - } - finalTrivia = finalTrivia.AddRange(leadingWhitespace); - - return node.WithLeadingTrivia(finalTrivia); + return GetNodeWithTrivia(leadingWhitespace, node, summary, parameters, typeParameters, remarks, seealsos, altmembers, relateds); } private SyntaxNode? VisitBaseMethodDeclaration(BaseMethodDeclarationSyntax node) @@ -209,8 +198,10 @@ private SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxN SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); - return GetNodeWithTrivia(leadingWhitespace, node, summary, parameters, typeParameters, returns, remarks, exceptions, seealsos); + return GetNodeWithTrivia(leadingWhitespace, node, summary, parameters, typeParameters, returns, remarks, exceptions, seealsos, altmembers, relateds); } private SyntaxNode? VisitMemberDeclaration(MemberDeclarationSyntax node) @@ -225,8 +216,60 @@ private SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxN SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); - return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks, exceptions); + return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks, exceptions, 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 summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); + + return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks, seealsos, altmembers, relateds); + } + + return node; + } + + private SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxNode node, params SyntaxTriviaList[] trivias) + { + SyntaxTriviaList finalTrivia = new(); + var leadingTrivia = node.GetLeadingTrivia(); + if (leadingTrivia.Any()) + { + if (leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) + { + // Ensure the endline that separates nodes is respected + finalTrivia = new(SyntaxFactory.ElasticCarriageReturnLineFeed); + } + } + + foreach (SyntaxTriviaList t in trivias) + { + finalTrivia = finalTrivia.AddRange(t); + } + finalTrivia = finalTrivia.AddRange(leadingWhitespace); + + return node.WithLeadingTrivia(finalTrivia); } private SyntaxTriviaList GetLeadingWhitespace(SyntaxNode node) => @@ -234,7 +277,7 @@ private SyntaxTriviaList GetLeadingWhitespace(SyntaxNode node) => private SyntaxTriviaList GetSummary(string text, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); + SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); XmlElementSyntax element = SyntaxFactory.XmlSummaryElement(contents); return GetXmlTrivia(element, leadingWhitespace); } @@ -256,40 +299,40 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp private SyntaxTriviaList GetValue(string text, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); + SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); XmlElementSyntax element = SyntaxFactory.XmlValueElement(contents); return GetXmlTrivia(element, leadingWhitespace); } - private SyntaxTriviaList GetParam(string name, string text, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetParameter(string name, string text, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); + SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); XmlElementSyntax element = SyntaxFactory.XmlParamElement(name, contents); return GetXmlTrivia(element, leadingWhitespace); } - private SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace) - { - var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute("name", name)); - SyntaxList contents = GetContentsInRows(text); - return GetXmlTrivia("typeparam", attribute, contents, leadingWhitespace); - } - - private SyntaxTriviaList GetParameters(DocsMember member, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList parameters = new(); - foreach (SyntaxTriviaList parameterTrivia in member.Params.Select( - param => GetParam(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) + foreach (SyntaxTriviaList parameterTrivia in api.Params.Select( + param => GetParameter(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) { parameters = parameters.AddRange(parameterTrivia); } return parameters; } - private SyntaxTriviaList GetTypeParameters(DocsMember member, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace) + { + var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute("name", name)); + SyntaxList contents = GetContentsInRows(text); + return GetXmlTrivia("typeparam", attribute, contents, leadingWhitespace); + } + + private SyntaxTriviaList GetTypeParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList typeParameters = new(); - foreach (SyntaxTriviaList typeParameterTrivia in member.TypeParams.Select( + foreach (SyntaxTriviaList typeParameterTrivia in api.TypeParams.Select( typeParam => GetTypeParam(typeParam.Name, UseBoilerplate ? BoilerplateText : typeParam.Value, leadingWhitespace))) { typeParameters = typeParameters.AddRange(typeParameterTrivia); @@ -305,20 +348,15 @@ private SyntaxTriviaList GetReturns(string text, SyntaxTriviaList leadingWhitesp return new(); } - SyntaxList contents = GetContentsInRows(text.WithoutPrefixes()); + SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); XmlElementSyntax element = SyntaxFactory.XmlReturnsElement(contents); return GetXmlTrivia(element, leadingWhitespace); } private SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace) { - if (cref.Length > 2 && cref[1] == ':') - { - cref = cref[2..]; - } - - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); - XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text.WithoutPrefixes(), leadingWhitespace, addInitialNewLine: false)); + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutPrefix())); + XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text.WithoutPrefix(), leadingWhitespace, addInitialNewLine: false)); XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); return GetXmlTrivia(element, leadingWhitespace); } @@ -338,14 +376,20 @@ private SyntaxTriviaList GetExceptions(DocsMember member, SyntaxTriviaList leadi return exceptions; } - private SyntaxTriviaList GetSeeAlsos(DocsMember member, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace) + { + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutPrefix())); + XmlEmptyElementSyntax element = SyntaxFactory.XmlSeeAlsoElement(crefSyntax); + return GetXmlTrivia(element, leadingWhitespace); + } + + private SyntaxTriviaList GetSeeAlsos(DocsAPI api, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList seealsos = new(); - // No need to add exceptions in secondary files - if (!UseBoilerplate && member.SeeAlsos.Any()) + if (!UseBoilerplate && api.SeeAlsoCrefs.Any()) { - foreach (SyntaxTriviaList seealsoTrivia in member.SeeAlsos.Select( - s => GetSeeAlso(s.Cref, leadingWhitespace))) + foreach (SyntaxTriviaList seealsoTrivia in api.SeeAlsoCrefs.Select( + s => GetSeeAlso(s, leadingWhitespace))) { seealsos = seealsos.AddRange(seealsoTrivia); } @@ -353,16 +397,50 @@ private SyntaxTriviaList GetSeeAlsos(DocsMember member, SyntaxTriviaList leading return seealsos; } - private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetAltMember(string cref, SyntaxTriviaList leadingWhitespace) { - if (cref.Length > 2 && cref[1] == ':') + XmlAttributeSyntax attribute = SyntaxFactory.XmlTextAttribute("cref", cref.WithoutPrefix()); + XmlEmptyElementSyntax emptyElement = SyntaxFactory.XmlEmptyElement(SyntaxFactory.XmlName(SyntaxFactory.Identifier("altmember")), new SyntaxList(attribute)); + return GetXmlTrivia(emptyElement, leadingWhitespace); + } + + private SyntaxTriviaList GetAltMembers(DocsAPI api, SyntaxTriviaList leadingWhitespace) + { + SyntaxTriviaList altMembers = new(); + if (!UseBoilerplate && api.AltMembers.Any()) { - cref = cref[2..]; + foreach (SyntaxTriviaList altMemberTrivia in api.AltMembers.Select( + s => GetAltMember(s, leadingWhitespace))) + { + altMembers = altMembers.AddRange(altMemberTrivia); + } } + return altMembers; + } - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); - XmlEmptyElementSyntax element = SyntaxFactory.XmlSeeAlsoElement(crefSyntax); - return GetXmlTrivia(element, leadingWhitespace); + private SyntaxTriviaList GetRelated(string articleType, string href, string value, SyntaxTriviaList leadingWhitespace) + { + SyntaxList attributes = new(); + + attributes = attributes.Add(SyntaxFactory.XmlTextAttribute("type", articleType)); + attributes = attributes.Add(SyntaxFactory.XmlTextAttribute("href", href)); + + SyntaxList contents = GetContentsInRows(value); + return GetXmlTrivia("related", attributes, contents, leadingWhitespace); + } + + private SyntaxTriviaList GetRelateds(DocsAPI api, SyntaxTriviaList leadingWhitespace) + { + SyntaxTriviaList relateds = new(); + if (!UseBoilerplate && api.Relateds.Any()) + { + foreach (SyntaxTriviaList relatedsTrivia in api.Relateds.Select( + s => GetRelated(s.ArticleType, s.Href, s.Value, leadingWhitespace))) + { + relateds = relateds.AddRange(relatedsTrivia); + } + } + return relateds; } private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace, bool addInitialNewLine) diff --git a/Tests/PortToDocs/PortToDocsTestData.cs b/Tests/PortToDocs/PortToDocsTestData.cs index 5fc16c8..38a6d5d 100644 --- a/Tests/PortToDocs/PortToDocsTestData.cs +++ b/Tests/PortToDocs/PortToDocsTestData.cs @@ -7,12 +7,13 @@ internal class PortToDocsTestData : TestData { private const string TestDataRootDirPath = @"../../../PortToDocs/TestData"; private const string IntellisenseAndDllDirName = "IntelliSenseAndDLL"; - internal DirectoryInfo IntelliSenseAndDLLDir { get; set; } // Docs file with the interface from which the type inherits. internal string InterfaceFilePath { get; set; } + internal string DocsOriginFilePath { get; set; } + internal PortToDocsTestData( TestDirectory tempDir, string testDataDir, @@ -24,15 +25,13 @@ internal PortToDocsTestData( Assert.False(string.IsNullOrWhiteSpace(assemblyName)); Assert.False(string.IsNullOrWhiteSpace(typeName)); - Assembly = assemblyName; - Namespace = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; - Type = typeName; + namespaceName = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; IntelliSenseAndDLLDir = tempDir.CreateSubdirectory(IntellisenseAndDllDirName); - DirectoryInfo tripleSlashAssemblyDir = IntelliSenseAndDLLDir.CreateSubdirectory(Assembly); + DirectoryInfo tripleSlashAssemblyDir = IntelliSenseAndDLLDir.CreateSubdirectory(assemblyName); DocsDir = tempDir.CreateSubdirectory(DocsDirName); - DirectoryInfo docsAssemblyDir = DocsDir.CreateSubdirectory(Namespace); + DirectoryInfo docsAssemblyDir = DocsDir.CreateSubdirectory(namespaceName); string testDataPath = Path.Combine(TestDataRootDirPath, testDataDir); @@ -44,15 +43,15 @@ internal PortToDocsTestData( Assert.True(File.Exists(docsOriginalFilePath)); Assert.True(File.Exists(docsExpectedFilePath)); - OriginalFilePath = Path.Combine(tripleSlashAssemblyDir.FullName, $"{Type}.xml"); - ActualFilePath = Path.Combine(docsAssemblyDir.FullName, $"{Type}.xml"); + DocsOriginFilePath = Path.Combine(tripleSlashAssemblyDir.FullName, $"{typeName}.xml"); + ActualFilePath = Path.Combine(docsAssemblyDir.FullName, $"{typeName}.xml"); ExpectedFilePath = Path.Combine(tempDir.FullPath, "DocsExpected.xml"); - File.Copy(tripleSlashOriginalFilePath, OriginalFilePath); + File.Copy(tripleSlashOriginalFilePath, DocsOriginFilePath); File.Copy(docsOriginalFilePath, ActualFilePath); File.Copy(docsExpectedFilePath, ExpectedFilePath); - Assert.True(File.Exists(OriginalFilePath)); + Assert.True(File.Exists(DocsOriginFilePath)); Assert.True(File.Exists(ActualFilePath)); Assert.True(File.Exists(ExpectedFilePath)); diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs b/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs index 42a4835..9357b2b 100644 --- a/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs +++ b/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; +using System.IO; using Xunit; namespace Libraries.Tests @@ -22,41 +20,33 @@ internal PortToTripleSlashTestData( Assert.False(string.IsNullOrWhiteSpace(assemblyName)); Assert.False(string.IsNullOrWhiteSpace(typeName)); - Assembly = assemblyName; - Namespace = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; - Type = typeName; + namespaceName = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; ProjectDir = tempDir.CreateSubdirectory(ProjectDirName); DocsDir = tempDir.CreateSubdirectory(DocsDirName); - DirectoryInfo docsAssemblyDir = DocsDir.CreateSubdirectory(Namespace); + DirectoryInfo docsAssemblyDir = DocsDir.CreateSubdirectory(namespaceName); string testDataPath = Path.Combine(TestDataRootDirPath, testDataDir); - string docsOriginalFilePath = Path.Combine(testDataPath, "DocsOriginal.xml"); - string csOriginalFilePath = Path.Combine(testDataPath, "SourceOriginal.cs"); - string csExpectedFilePath = Path.Combine(testDataPath, "SourceExpected.cs"); - string csprojOriginalFilePath = Path.Combine(testDataPath, "Project.csproj"); - - Assert.True(File.Exists(docsOriginalFilePath)); - Assert.True(File.Exists(csOriginalFilePath)); - Assert.True(File.Exists(csExpectedFilePath)); - Assert.True(File.Exists(csprojOriginalFilePath)); - - OriginalFilePath = Path.Combine(docsAssemblyDir.FullName, $"{Type}.xml"); - ActualFilePath = Path.Combine(ProjectDir.FullName, $"{Type}.cs"); - ExpectedFilePath = Path.Combine(tempDir.FullPath, "SourceExpected.cs"); - ProjectFilePath = Path.Combine(ProjectDir.FullName, $"{Assembly}.csproj"); - - File.Copy(docsOriginalFilePath, OriginalFilePath); - File.Copy(csOriginalFilePath, ActualFilePath); - File.Copy(csExpectedFilePath, ExpectedFilePath); - File.Copy(csprojOriginalFilePath, ProjectFilePath); - - Assert.True(File.Exists(OriginalFilePath)); - Assert.True(File.Exists(ActualFilePath)); - Assert.True(File.Exists(ExpectedFilePath)); - Assert.True(File.Exists(ProjectFilePath)); + foreach (string origin in Directory.EnumerateFiles(testDataPath, "*.xml")) + { + string fileName = Path.GetFileName(origin); + string destination = Path.Combine(docsAssemblyDir.FullName, fileName); + File.Copy(origin, destination); + } + + string originCsOriginal = Path.Combine(testDataPath, $"SourceOriginal.cs"); + ActualFilePath = Path.Combine(ProjectDir.FullName, $"{typeName}.cs"); + File.Copy(originCsOriginal, ActualFilePath); + + string originCsExpected = Path.Combine(testDataPath, $"SourceExpected.cs"); + ExpectedFilePath = Path.Combine(tempDir.FullPath, $"SourceExpected.cs"); + File.Copy(originCsExpected, ExpectedFilePath); + + string originCsproj = Path.Combine(testDataPath, $"{assemblyName}.csproj"); + ProjectFilePath = Path.Combine(ProjectDir.FullName, $"{assemblyName}.csproj"); + File.Copy(originCsproj, ProjectFilePath); } } } diff --git a/Tests/PortToTripleSlash/TestData/Basic/Project.csproj b/Tests/PortToTripleSlash/TestData/Basic/MyAssembly.csproj similarity index 100% rename from Tests/PortToTripleSlash/TestData/Basic/Project.csproj rename to Tests/PortToTripleSlash/TestData/Basic/MyAssembly.csproj diff --git a/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml b/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml new file mode 100644 index 0000000..42af915 --- /dev/null +++ b/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml @@ -0,0 +1,22 @@ + + + + MyAssembly + + + + + + + System.Void + + + This is the sender parameter. + This is the e parameter. + This is the MyDelegate summary. + To be added. + + + The .NET Runtime repo. + + \ No newline at end of file diff --git a/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml similarity index 92% rename from Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml rename to Tests/PortToTripleSlash/TestData/Basic/MyType.xml index b0e4026..c40b945 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/DocsOriginal.xml +++ b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml @@ -158,5 +158,18 @@ Mentions the . To be added. + + + Event + + MyAssembly + + + MyNamespace.MyDelegate + + + This is the MyEvent summary. + + \ No newline at end of file diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index 02fde4d..d10f687 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -100,5 +100,16 @@ public void MyVoidMethod() public void MyTypeParamMethod() { } + + /// This is the MyDelegate summary. + /// This is the sender parameter. + /// This is the e parameter. + /// + /// + /// The .NET Runtime repo. + public delegate void MyDelegate(object sender, object e); + + /// This is the MyEvent summary. + public event MyDelegate MyEvent; } } diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs index f5b59f2..2993f5e 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs @@ -35,5 +35,9 @@ public void MyVoidMethod() public void MyTypeParamMethod() { } + + public delegate void MyDelegate(object sender, object e); + + public event MyDelegate MyEvent; } } diff --git a/Tests/TestData.cs b/Tests/TestData.cs index ac45c7d..ca56dd5 100644 --- a/Tests/TestData.cs +++ b/Tests/TestData.cs @@ -4,19 +4,14 @@ namespace Libraries.Tests { internal class TestData { - public const string TestAssembly = "MyAssembly"; - public const string TestNamespace = "MyNamespace"; - public const string TestType = "MyType"; + internal const string TestAssembly = "MyAssembly"; + internal const string TestNamespace = "MyNamespace"; + internal const string TestType = "MyType"; + internal const string DocsDirName = "Docs"; - protected const string DocsDirName = "Docs"; - - protected string Assembly { get; set; } - protected string Namespace { get; set; } - protected string Type { get; set; } - - internal DirectoryInfo DocsDir { get; set; } - internal string OriginalFilePath { get; set; } internal string ExpectedFilePath { get; set; } internal string ActualFilePath { get; set; } + internal DirectoryInfo DocsDir { get; set; } + } } diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index ee0ca9b..fdf6811 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -34,7 +34,7 @@ - + From 7a21a476f471fccaaff07519e10638edc6b2c94f Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 7 Jan 2021 15:51:39 -0800 Subject: [PATCH 49/65] Fix some more exceptions vs errors. Remove boilerplate message. Avoid adding ## Remarks. Fix bug preventing exception comments from being added. --- Libraries/Configuration.cs | 20 +-- Libraries/Docs/DocsCommentsContainer.cs | 3 +- Libraries/Docs/DocsMember.cs | 3 +- Libraries/Docs/DocsType.cs | 3 +- .../IntelliSenseXmlCommentsContainer.cs | 3 +- Libraries/Log.cs | 20 ++- .../TripleSlashSyntaxRewriter.cs | 142 ++++++++---------- Libraries/ToDocsPorter.cs | 2 +- Libraries/ToTripleSlashPorter.cs | 40 +++-- Libraries/XmlHelper.cs | 27 ++-- .../PortToTripleSlashTests.cs | 4 +- .../TestData/Basic/SourceExpected.cs | 10 -- 12 files changed, 133 insertions(+), 144 deletions(-) diff --git a/Libraries/Configuration.cs b/Libraries/Configuration.cs index e097e6e..9424909 100644 --- a/Libraries/Configuration.cs +++ b/Libraries/Configuration.cs @@ -120,18 +120,18 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) { if (string.IsNullOrWhiteSpace(arg)) { - Log.Error("You must specify a *.csproj path."); + throw new Exception("You must specify a *.csproj path."); } else if (!File.Exists(arg)) { - Log.Error($"The *.csproj file does not exist: {arg}"); + throw new Exception($"The *.csproj file does not exist: {arg}"); } else { string ext = Path.GetExtension(arg).ToUpperInvariant(); if (ext != ".CSPROJ") { - Log.Error($"The file does not have a *.csproj extension: {arg}"); + throw new Exception($"The file does not have a *.csproj extension: {arg}"); } } config.CsProj = new FileInfo(arg); @@ -155,9 +155,11 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) break; case "TOTRIPLESLASH": config.Direction = PortingDirection.ToTripleSlash; + // Must always skip to avoid loading interface docs files to memory + config.SkipInterfaceImplementations = true; break; default: - Log.Error($"Unrecognized direction value: {arg}"); + throw new Exception($"Unrecognized direction value: {arg}"); break; } mode = Mode.Initial; @@ -174,7 +176,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) DirectoryInfo dirInfo = new DirectoryInfo(dirPath); if (!dirInfo.Exists) { - Log.Error($"This Docs xml directory does not exist: {dirPath}"); + throw new Exception($"This Docs xml directory does not exist: {dirPath}"); } config.DirsDocsXml.Add(dirInfo); @@ -189,11 +191,11 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) { if (!int.TryParse(arg, out int value)) { - Log.Error($"Invalid int value for 'Exception collision threshold' argument: {arg}"); + throw new Exception($"Invalid int value for 'Exception collision threshold' argument: {arg}"); } else if (value < 1 || value > 100) { - Log.Error($"Value needs to be between 0 and 100: {value}"); + throw new Exception($"Value needs to be between 0 and 100: {value}"); } config.ExceptionCollisionThreshold = value; @@ -478,7 +480,7 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) DirectoryInfo dirInfo = new DirectoryInfo(dirPath); if (!dirInfo.Exists) { - Log.Error($"This IntelliSense directory does not exist: {dirPath}"); + throw new Exception($"This IntelliSense directory does not exist: {dirPath}"); } config.DirsIntelliSense.Add(dirInfo); @@ -648,7 +650,7 @@ private static bool ParseOrExit(string arg, string paramFriendlyName) { if (!bool.TryParse(arg, out bool value)) { - Log.Error($"Invalid boolean value for '{paramFriendlyName}' argument: {arg}"); + throw new Exception($"Invalid boolean value for '{paramFriendlyName}' argument: {arg}"); } Log.Cyan($"{paramFriendlyName}:"); diff --git a/Libraries/Docs/DocsCommentsContainer.cs b/Libraries/Docs/DocsCommentsContainer.cs index 0d9fc44..958ee61 100644 --- a/Libraries/Docs/DocsCommentsContainer.cs +++ b/Libraries/Docs/DocsCommentsContainer.cs @@ -175,8 +175,7 @@ private void LoadFile(FileInfo fileInfo) { if (!fileInfo.Exists) { - Log.Error($"Docs xml file does not exist: {fileInfo.FullName}"); - return; + throw new Exception($"Docs xml file does not exist: {fileInfo.FullName}"); } xDoc = XDocument.Load(fileInfo.FullName); diff --git a/Libraries/Docs/DocsMember.cs b/Libraries/Docs/DocsMember.cs index 75e5a0a..4b919a3 100644 --- a/Libraries/Docs/DocsMember.cs +++ b/Libraries/Docs/DocsMember.cs @@ -63,8 +63,7 @@ public override string DocId if (ms == null) { string message = string.Format("Could not find a DocId MemberSignature for '{0}'", MemberName); - Log.Error(message); - throw new MissingMemberException(message); + throw new Exception(message); } _docId = ms.Value; } diff --git a/Libraries/Docs/DocsType.cs b/Libraries/Docs/DocsType.cs index 6731f87..6c89ccf 100644 --- a/Libraries/Docs/DocsType.cs +++ b/Libraries/Docs/DocsType.cs @@ -90,8 +90,7 @@ public override string DocId if (dts == null) { string message = $"DocId TypeSignature not found for FullName"; - Log.Error($"DocId TypeSignature not found for FullName"); - throw new MissingMemberException(message); + throw new Exception($"DocId TypeSignature not found for FullName"); } _docId = dts.Value; } diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs index 9a167ba..7a9ce6b 100644 --- a/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs @@ -1,4 +1,5 @@ #nullable enable +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -86,7 +87,7 @@ private void LoadFile(FileInfo fileInfo, bool printSuccess) { if (!fileInfo.Exists) { - Log.Error($"The IntelliSense xml file does not exist: {fileInfo.FullName}"); + throw new Exception($"The IntelliSense xml file does not exist: {fileInfo.FullName}"); return; } diff --git a/Libraries/Log.cs b/Libraries/Log.cs index 822e7cf..f98cd79 100644 --- a/Libraries/Log.cs +++ b/Libraries/Log.cs @@ -102,11 +102,6 @@ public static void Error(string format, params object[]? args) public static void Error(bool endline, string format, params object[]? args) { Print(endline, ConsoleColor.Red, format, args); - - if (args == null) - throw new Exception(format); - else - throw new Exception(string.Format(format, args)); } public static void Cyan(string format) @@ -139,6 +134,11 @@ public static void Cyan(bool endline, string format, params object[]? args) Print(endline, ConsoleColor.Cyan, format, args); } + public static void Assert(bool condition, string format) + { + Assert(true, condition, format, null); + } + public static void Assert(bool condition, string format, params object[]? args) { Assert(true, condition, format, args); @@ -152,7 +152,8 @@ public static void Assert(bool endline, bool condition, string format, params ob } else { - Error(endline, format, args); + string msg = args != null ? string.Format(format, args) : format; + throw new Exception(msg); } } @@ -167,6 +168,11 @@ public static void PrintHelpAndError(string format, params object[]? args) { PrintHelp(); Error(format, args); + + if (args == null) + throw new Exception(format); + else + throw new Exception(string.Format(format, args)); } public static void PrintHelp() @@ -247,6 +253,8 @@ Determines in which direction the comments should flow. to the specified Docs repo containing ECMA xml files. > ToTripleSlash: Comments are ported from the specified Docs repo containint ECMA xml files, to the triple slash comments on top of each API in the specified source code repo. + Using this option automatically sets `SkipInterfaceImplementations` to `true`, to avoid loading + unnecessary interface docs xml files into memory. Usage example: -Direction ToTripleSlash diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index fe166f8..cce596a 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -12,17 +12,13 @@ namespace Libraries.RoslynTripleSlash { internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter { - private const string BoilerplateText = "Comments located in main file."; - private DocsCommentsContainer DocsComments { get; } private SemanticModel Model { get; } - private bool UseBoilerplate { get; } - public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticModel model, Location location, SyntaxTree tree, bool useBoilerplate) : base(visitIntoStructuredTrivia: true) + public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticModel model, Location location, SyntaxTree tree) : base(visitIntoStructuredTrivia: true) { DocsComments = docsComments; Model = model; - UseBoilerplate = useBoilerplate; } #region Visitor overrides @@ -94,24 +90,15 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod return node; } - string summaryText = BoilerplateText; - string valueText = BoilerplateText; - - if (!UseBoilerplate) - { - summaryText = member.Summary; - valueText = member.Value; - } - SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(summaryText, leadingWhitespace); - SyntaxTriviaList value = GetValue(valueText, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); + SyntaxTriviaList value = GetValue(member.Value, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); - SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); - SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); - SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); - SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); + SyntaxTriviaList exceptions = GetExceptions(member.Exceptions, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace); return GetNodeWithTrivia(leadingWhitespace, node, summary, value, remarks, exceptions, seealsos, altmembers, relateds); } @@ -146,36 +133,22 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod return node; } - string summaryText = BoilerplateText; - string remarksText = string.Empty; - - SyntaxTriviaList parameters = new(); - SyntaxTriviaList typeParameters = new(); - SyntaxTriviaList seealsos = new(); - SyntaxTriviaList altmembers = new(); - SyntaxTriviaList relateds = new(); - SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - if (!UseBoilerplate) + if (!TryGetType(symbol, out DocsType? type)) { - if (!TryGetType(symbol, out DocsType? type)) - { - return node; - } - - summaryText = type.Summary; - remarksText = type.Remarks; - - parameters = GetParameters(type, leadingWhitespace); - typeParameters = GetTypeParameters(type, leadingWhitespace); - seealsos = GetSeeAlsos(type, leadingWhitespace); - altmembers = GetAltMembers(type, leadingWhitespace); - relateds = GetRelateds(type, leadingWhitespace); + return node; } - SyntaxTriviaList summary = GetSummary(summaryText, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(remarksText, leadingWhitespace); + + SyntaxTriviaList summary = GetSummary(type.Summary, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(type.Remarks, leadingWhitespace); + SyntaxTriviaList parameters = GetParameters(type, leadingWhitespace); + SyntaxTriviaList typeParameters = GetTypeParameters(type, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(type.SeeAlsoCrefs, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(type.AltMembers, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(type.Relateds, leadingWhitespace); + return GetNodeWithTrivia(leadingWhitespace, node, summary, parameters, typeParameters, remarks, seealsos, altmembers, relateds); } @@ -191,15 +164,15 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); SyntaxTriviaList parameters = GetParameters(member, leadingWhitespace); SyntaxTriviaList typeParameters = GetTypeParameters(member, leadingWhitespace); - SyntaxTriviaList returns = GetReturns(UseBoilerplate ? BoilerplateText : member.Returns, leadingWhitespace); + SyntaxTriviaList returns = GetReturns(member.Returns, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); - SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); - SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); - SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); - SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); + SyntaxTriviaList exceptions = GetExceptions(member.Exceptions, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace); return GetNodeWithTrivia(leadingWhitespace, node, summary, parameters, typeParameters, returns, remarks, exceptions, seealsos, altmembers, relateds); } @@ -213,12 +186,12 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); - SyntaxTriviaList exceptions = GetExceptions(member, leadingWhitespace); - SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); - SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); - SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); + SyntaxTriviaList exceptions = GetExceptions(member.Exceptions, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace); return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks, exceptions, seealsos, altmembers, relateds); } @@ -238,11 +211,11 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(UseBoilerplate ? BoilerplateText : member.Summary, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); - SyntaxTriviaList seealsos = GetSeeAlsos(member, leadingWhitespace); - SyntaxTriviaList altmembers = GetAltMembers(member, leadingWhitespace); - SyntaxTriviaList relateds = GetRelateds(member, leadingWhitespace); + SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); + SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); + SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace); return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks, seealsos, altmembers, relateds); } @@ -284,10 +257,10 @@ private SyntaxTriviaList GetSummary(string text, SyntaxTriviaList leadingWhitesp private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitespace) { - if (!UseBoilerplate && !text.IsDocsEmpty()) + if (!text.IsDocsEmpty()) { string trimmedRemarks = text.RemoveSubstrings("").Trim(); // The SyntaxFactory needs to be the one to add these - SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true); + SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true, removeRemarksHeader: true); XmlNodeSyntax xmlRemarksContent = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(xmlRemarksContent); @@ -315,7 +288,7 @@ private SyntaxTriviaList GetParameters(DocsAPI api, SyntaxTriviaList leadingWhit { SyntaxTriviaList parameters = new(); foreach (SyntaxTriviaList parameterTrivia in api.Params.Select( - param => GetParameter(param.Name, UseBoilerplate ? BoilerplateText : param.Value, leadingWhitespace))) + param => GetParameter(param.Name, param.Value, leadingWhitespace))) { parameters = parameters.AddRange(parameterTrivia); } @@ -333,7 +306,7 @@ private SyntaxTriviaList GetTypeParameters(DocsAPI api, SyntaxTriviaList leading { SyntaxTriviaList typeParameters = new(); foreach (SyntaxTriviaList typeParameterTrivia in api.TypeParams.Select( - typeParam => GetTypeParam(typeParam.Name, UseBoilerplate ? BoilerplateText : typeParam.Value, leadingWhitespace))) + typeParam => GetTypeParam(typeParam.Name, typeParam.Value, leadingWhitespace))) { typeParameters = typeParameters.AddRange(typeParameterTrivia); } @@ -361,13 +334,13 @@ private SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList return GetXmlTrivia(element, leadingWhitespace); } - private SyntaxTriviaList GetExceptions(DocsMember member, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetExceptions(List docsExceptions, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList exceptions = new(); // No need to add exceptions in secondary files - if (!UseBoilerplate && member.Exceptions.Any()) + if (docsExceptions.Any()) { - foreach (SyntaxTriviaList exceptionsTrivia in member.Exceptions.Select( + foreach (SyntaxTriviaList exceptionsTrivia in docsExceptions.Select( exception => GetException(exception.Cref, exception.Value, leadingWhitespace))) { exceptions = exceptions.AddRange(exceptionsTrivia); @@ -383,12 +356,12 @@ private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitesp return GetXmlTrivia(element, leadingWhitespace); } - private SyntaxTriviaList GetSeeAlsos(DocsAPI api, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetSeeAlsos(List docsSeeAlsoCrefs, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList seealsos = new(); - if (!UseBoilerplate && api.SeeAlsoCrefs.Any()) + if (docsSeeAlsoCrefs.Any()) { - foreach (SyntaxTriviaList seealsoTrivia in api.SeeAlsoCrefs.Select( + foreach (SyntaxTriviaList seealsoTrivia in docsSeeAlsoCrefs.Select( s => GetSeeAlso(s, leadingWhitespace))) { seealsos = seealsos.AddRange(seealsoTrivia); @@ -404,12 +377,12 @@ private SyntaxTriviaList GetAltMember(string cref, SyntaxTriviaList leadingWhite return GetXmlTrivia(emptyElement, leadingWhitespace); } - private SyntaxTriviaList GetAltMembers(DocsAPI api, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetAltMembers(List docsAltMembers, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList altMembers = new(); - if (!UseBoilerplate && api.AltMembers.Any()) + if (docsAltMembers.Any()) { - foreach (SyntaxTriviaList altMemberTrivia in api.AltMembers.Select( + foreach (SyntaxTriviaList altMemberTrivia in docsAltMembers.Select( s => GetAltMember(s, leadingWhitespace))) { altMembers = altMembers.AddRange(altMemberTrivia); @@ -429,12 +402,12 @@ private SyntaxTriviaList GetRelated(string articleType, string href, string valu return GetXmlTrivia("related", attributes, contents, leadingWhitespace); } - private SyntaxTriviaList GetRelateds(DocsAPI api, SyntaxTriviaList leadingWhitespace) + private SyntaxTriviaList GetRelateds(List docsRelateds, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList relateds = new(); - if (!UseBoilerplate && api.Relateds.Any()) + if (docsRelateds.Any()) { - foreach (SyntaxTriviaList relatedsTrivia in api.Relateds.Select( + foreach (SyntaxTriviaList relatedsTrivia in docsRelateds.Select( s => GetRelated(s.ArticleType, s.Href, s.Value, leadingWhitespace))) { relateds = relateds.AddRange(relatedsTrivia); @@ -443,7 +416,7 @@ private SyntaxTriviaList GetRelateds(DocsAPI api, SyntaxTriviaList leadingWhites return relateds; } - private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace, bool addInitialNewLine) + private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace, bool addInitialNewLine, bool removeRemarksHeader = false) { string whitespace = leadingWhitespace.ToFullString().Replace(Environment.NewLine, ""); SyntaxToken newLineAndWhitespace = SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace); @@ -463,9 +436,18 @@ private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhi tokens.Add(newLineAndWhitespace); } - int lineNumber = 1; - foreach (string line in splittedLines) + for (int lineNumber = 0; lineNumber < splittedLines.Length; lineNumber++) { + string line = splittedLines[lineNumber]; + + if (removeRemarksHeader && + (line.Contains("## Remarks") || line.Contains("##Remarks"))) + { + // Avoid adding the '## Remarks' header, it's unnecessary + removeRemarksHeader = false; + continue; + } + SyntaxToken token = SyntaxFactory.XmlTextLiteral(leading, line, line, default); tokens.Add(token); @@ -475,8 +457,6 @@ private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhi tokens.Add(newLineAndWhitespace); tokens.Add(newLineAndWhitespace); } - - lineNumber++; } return SyntaxFactory.TokenList(tokens); } diff --git a/Libraries/ToDocsPorter.cs b/Libraries/ToDocsPorter.cs index a3f87d1..67828a3 100644 --- a/Libraries/ToDocsPorter.cs +++ b/Libraries/ToDocsPorter.cs @@ -313,7 +313,7 @@ private void TryPortMissingParamsForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlMe created = TryPromptParam(dParam, tsMemberToPort, out IntelliSenseXmlParam? newTsParam); if (newTsParam == null) { - Log.Error($" There param '{dParam.Name}' was not found in IntelliSense xml for {dApiToUpdate.DocId}"); + Log.Error($" The param '{dParam.Name}' was not found in IntelliSense xml for {dApiToUpdate.DocId}."); } else { diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index 81edf02..0f70755 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -1,17 +1,16 @@ #nullable enable using Libraries.Docs; using Libraries.RoslynTripleSlash; +using Microsoft.Build.Locator; using Microsoft.Build.Logging; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.MSBuild; using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Reflection; -using System.Linq; -using Microsoft.Build.Locator; -using System.Collections.Generic; using System.Runtime.Loader; namespace Libraries @@ -57,6 +56,8 @@ public void Start() throw new Exception("The MSBuild directory was not found in PATH. Use '-MSBuild ' to specify it."); } + CheckDiagnostics(workspace, "MSBuildWorkspace.Create"); + BinaryLogger? binLogger = null; if (Config.BinLogger) { @@ -74,26 +75,45 @@ public void Start() throw new Exception("Could not find a project."); } + CheckDiagnostics(workspace, "workspace.OpenProjectAsync"); + Compilation? compilation = project.GetCompilationAsync().Result; if (compilation == null) { throw new NullReferenceException("The project's compilation was null."); } + CheckDiagnostics(workspace, "project.GetCompilationAsync"); + + PortCommentsForProject(compilation!); + } + + private void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) + { ImmutableList diagnostics = workspace.Diagnostics; if (diagnostics.Any()) { - string allMsgs = Environment.NewLine; + string initialMsg = $"Diagnostic messages found in {stepName}:"; + Log.Error(initialMsg); + + List allMsgs = new() { initialMsg }; + foreach (var diagnostic in diagnostics) { string msg = $"{diagnostic.Kind} - {diagnostic.Message}"; Log.Error(msg); - allMsgs += msg + Environment.NewLine; + + if (!msg.Contains("Warning - Found project reference without a matching metadata reference")) + { + allMsgs.Add(msg); + } } - throw new Exception("Exiting due to diagnostic errors found: " + allMsgs); - } - PortCommentsForProject(compilation!); + if (allMsgs.Count > 1) + { + throw new Exception("Exiting due to diagnostic errors found: " + Environment.NewLine + string.Join(Environment.NewLine, allMsgs)); + } + } } private void PortCommentsForProject(Compilation compilation) @@ -116,7 +136,6 @@ private void PortCommentsForProject(Compilation compilation) private void PortCommentsForType(Compilation compilation, IDocsAPI api, ISymbol symbol) { - bool useBoilerplate = false; foreach (Location location in symbol.Locations) { SyntaxTree? tree = location.SourceTree; @@ -127,7 +146,7 @@ private void PortCommentsForType(Compilation compilation, IDocsAPI api, ISymbol } SemanticModel model = compilation.GetSemanticModel(tree); - var rewriter = new TripleSlashSyntaxRewriter(DocsComments, model, location, tree, useBoilerplate); + var rewriter = new TripleSlashSyntaxRewriter(DocsComments, model, location, tree); SyntaxNode? newRoot = rewriter.Visit(tree.GetRoot()); if (newRoot == null) { @@ -136,7 +155,6 @@ private void PortCommentsForType(Compilation compilation, IDocsAPI api, ISymbol } File.WriteAllText(tree.FilePath, newRoot.ToFullString()); - useBoilerplate = true; } } diff --git a/Libraries/XmlHelper.cs b/Libraries/XmlHelper.cs index cbf6a6c..198e667 100644 --- a/Libraries/XmlHelper.cs +++ b/Libraries/XmlHelper.cs @@ -87,8 +87,7 @@ public static string GetAttributeValue(XElement parent, string name) { if (parent == null) { - Log.Error("A null parent was passed when attempting to get attribute '{0}'", name); - throw new ArgumentNullException(nameof(parent)); + throw new Exception($"A null parent was passed when attempting to get attribute '{name}'"); } else { @@ -129,8 +128,7 @@ public static string GetNodesInPlainText(XElement element) { if (element == null) { - Log.Error("A null element was passed when attempting to retrieve the nodes in plain text."); - throw new ArgumentNullException(nameof(element)); + throw new Exception("A null element was passed when attempting to retrieve the nodes in plain text."); } return string.Join("", element.Nodes()).Trim(); } @@ -139,8 +137,7 @@ public static void SaveFormattedAsMarkdown(XElement element, string newValue, bo { if (element == null) { - Log.Error("A null element was passed when attempting to save formatted as markdown"); - throw new ArgumentNullException(nameof(element)); + 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 @@ -172,14 +169,12 @@ public static void AddChildFormattedAsMarkdown(XElement parent, XElement child, { if (parent == null) { - Log.Error("A null parent was passed when attempting to add child formatted as markdown"); - throw new ArgumentNullException(nameof(parent)); + throw new Exception("A null parent was passed when attempting to add child formatted as markdown."); } if (child == null) { - Log.Error("A null child was passed when attempting to add child formatted as markdown"); - throw new ArgumentNullException(nameof(child)); + throw new Exception("A null child was passed when attempting to add child formatted as markdown."); } SaveFormattedAsMarkdown(child, childValue, isMember); @@ -190,8 +185,7 @@ public static void SaveFormattedAsXml(XElement element, string newValue, bool re { if (element == null) { - Log.Error("A null element was passed when attempting to save formatted as xml"); - throw new ArgumentNullException(nameof(element)); + throw new Exception("A null element was passed when attempting to save formatted as xml"); } element.Value = string.Empty; @@ -222,8 +216,7 @@ public static void AppendFormattedAsXml(XElement element, string valueToAppend, { if (element == null) { - Log.Error("A null element was passed when attempting to append formatted as xml"); - throw new ArgumentNullException(nameof(element)); + throw new Exception("A null element was passed when attempting to append formatted as xml"); } SaveFormattedAsXml(element, GetNodesInPlainText(element) + valueToAppend, removeUndesiredEndlines); @@ -233,14 +226,12 @@ public static void AddChildFormattedAsXml(XElement parent, XElement child, strin { if (parent == null) { - Log.Error("A null parent was passed when attempting to add child formatted as xml"); - throw new ArgumentNullException(nameof(parent)); + throw new Exception("A null parent was passed when attempting to add child formatted as xml"); } if (child == null) { - Log.Error("A null child was passed when attempting to add child formatted as xml"); - throw new ArgumentNullException(nameof(child)); + throw new Exception("A null child was passed when attempting to add child formatted as xml"); } SaveFormattedAsXml(child, childValue); diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs index d1a9554..0b188ca 100644 --- a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs +++ b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs @@ -14,6 +14,7 @@ public void Port_Basic() private void PortToTripleSlash( string testDataDir, bool save = true, + bool skipInterfaceImplementations = true, string assemblyName = TestData.TestAssembly, string namespaceName = TestData.TestNamespace, string typeName = TestData.TestType) @@ -31,7 +32,8 @@ private void PortToTripleSlash( { Direction = Configuration.PortingDirection.ToTripleSlash, CsProj = new FileInfo(testData.ProjectFilePath), - Save = save + Save = save, + SkipInterfaceImplementations = skipInterfaceImplementations }; c.IncludedAssemblies.Add(assemblyName); diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index d10f687..6eb82f8 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -5,8 +5,6 @@ namespace MyNamespace /// This is the MyType class summary. /// This is the MyProperty value. /// This is the MyField summary. /// This is the MyIntMethod return value. It mentions the . /// This is the MyVoidMethod summary. /// Date: Fri, 8 Jan 2021 20:27:27 -0800 Subject: [PATCH 50/65] Make some simplifications on remarks based on feedback. Fix nullability warnings. Obtain all projects related to all relevant symbols. --- Libraries/Configuration.cs | 1 - Libraries/Docs/DocsAPI.cs | 2 +- Libraries/Docs/DocsMember.cs | 10 +- .../IntelliSenseXmlCommentsContainer.cs | 1 - .../IntelliSenseXml/IntelliSenseXmlMember.cs | 8 +- .../TripleSlashSyntaxRewriter.cs | 15 +- Libraries/ToTripleSlashPorter.cs | 194 ++++++++++++------ Libraries/XmlHelper.cs | 4 +- Program/DocsPortingTool.cs | 4 +- .../TestData/Basic/SourceExpected.cs | 17 -- 10 files changed, 148 insertions(+), 108 deletions(-) diff --git a/Libraries/Configuration.cs b/Libraries/Configuration.cs index 9424909..f2843ac 100644 --- a/Libraries/Configuration.cs +++ b/Libraries/Configuration.cs @@ -160,7 +160,6 @@ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args) break; default: throw new Exception($"Unrecognized direction value: {arg}"); - break; } mode = Mode.Initial; break; diff --git a/Libraries/Docs/DocsAPI.cs b/Libraries/Docs/DocsAPI.cs index 64b2a32..f29246d 100644 --- a/Libraries/Docs/DocsAPI.cs +++ b/Libraries/Docs/DocsAPI.cs @@ -76,7 +76,7 @@ public XElement Docs { get { - return XERoot.Element("Docs"); + return XERoot.Element("Docs") ?? throw new NullReferenceException($"Docs section was null in {FilePath}"); } } diff --git a/Libraries/Docs/DocsMember.cs b/Libraries/Docs/DocsMember.cs index 4b919a3..e6345d7 100644 --- a/Libraries/Docs/DocsMember.cs +++ b/Libraries/Docs/DocsMember.cs @@ -83,12 +83,8 @@ public string ImplementsInterfaceMember { get { - XElement xeImplements = XERoot.Element("Implements"); - if (xeImplements != null) - { - return XmlHelper.GetChildElementValue(xeImplements, "InterfaceMember"); - } - return string.Empty; + XElement? xeImplements = XERoot.Element("Implements"); + return (xeImplements != null) ? XmlHelper.GetChildElementValue(xeImplements, "InterfaceMember") : string.Empty; } } @@ -96,7 +92,7 @@ public string ReturnType { get { - XElement xeReturnValue = XERoot.Element("ReturnValue"); + XElement? xeReturnValue = XERoot.Element("ReturnValue"); if (xeReturnValue != null) { return XmlHelper.GetChildElementValue(xeReturnValue, "ReturnType"); diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs index 7a9ce6b..e6b5a55 100644 --- a/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs @@ -88,7 +88,6 @@ private void LoadFile(FileInfo fileInfo, bool printSuccess) if (!fileInfo.Exists) { throw new Exception($"The IntelliSense xml file does not exist: {fileInfo.FullName}"); - return; } xDoc = XDocument.Load(fileInfo.FullName); diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs index c0c56e8..17157e7 100644 --- a/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs +++ b/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs @@ -92,7 +92,7 @@ public string Summary { if (_summary == null) { - XElement xElement = XEMember.Element("summary"); + XElement? xElement = XEMember.Element("summary"); _summary = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; } return _summary; @@ -106,7 +106,7 @@ public string Value { if (_value == null) { - XElement xElement = XEMember.Element("value"); + XElement? xElement = XEMember.Element("value"); _value = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; } return _value; @@ -120,7 +120,7 @@ public string Returns { if (_returns == null) { - XElement xElement = XEMember.Element("returns"); + XElement? xElement = XEMember.Element("returns"); _returns = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; } return _returns; @@ -134,7 +134,7 @@ public string Remarks { if (_remarks == null) { - XElement xElement = XEMember.Element("remarks"); + XElement? xElement = XEMember.Element("remarks"); _remarks = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty; } return _remarks; diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index cce596a..d8e8aa1 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -260,7 +260,7 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp if (!text.IsDocsEmpty()) { string trimmedRemarks = text.RemoveSubstrings("").Trim(); // The SyntaxFactory needs to be the one to add these - SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true, removeRemarksHeader: true); + SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true, remarks: true); XmlNodeSyntax xmlRemarksContent = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(xmlRemarksContent); @@ -416,7 +416,7 @@ private SyntaxTriviaList GetRelateds(List docsRelateds, SyntaxTrivi return relateds; } - private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace, bool addInitialNewLine, bool removeRemarksHeader = false) + private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace, bool addInitialNewLine, bool remarks = false) { string whitespace = leadingWhitespace.ToFullString().Replace(Environment.NewLine, ""); SyntaxToken newLineAndWhitespace = SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace); @@ -431,8 +431,6 @@ private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhi // Only add the initial new line and whitespace if the contents have more than one line. Otherwise, we want the contents to be inlined inside the tags. if (splittedLines.Length > 1 && addInitialNewLine) { - // For example, the remarks section needs a new line before the initial "## Remarks" title - tokens.Add(newLineAndWhitespace); tokens.Add(newLineAndWhitespace); } @@ -440,11 +438,11 @@ private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhi { string line = splittedLines[lineNumber]; - if (removeRemarksHeader && - (line.Contains("## Remarks") || line.Contains("##Remarks"))) + // Avoid adding the '## Remarks' header, it's unnecessary + if (remarks && (line.Contains("## Remarks") || line.Contains("##Remarks"))) { - // Avoid adding the '## Remarks' header, it's unnecessary - removeRemarksHeader = false; + // Reduces the number of Contains calls + remarks = false; continue; } @@ -455,7 +453,6 @@ private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhi if (splittedLines.Length > 1) { tokens.Add(newLineAndWhitespace); - tokens.Add(newLineAndWhitespace); } } return SyntaxFactory.TokenList(tokens); diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index 0f70755..8d5da3b 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -17,9 +17,50 @@ namespace Libraries { public class ToTripleSlashPorter { + private struct ProjectData + { + public MSBuildWorkspace Workspace; + public Project Project; + public Compilation Compilation; + } + + private struct SymbolData + { + public ProjectData ProjectData; + public DocsType Api; + } + private readonly Configuration Config; private readonly DocsCommentsContainer DocsComments; - private VisualStudioInstance MSBuildInstance; + private readonly VisualStudioInstance MSBuildInstance; + + private List ProjectDatas = new(); +#pragma warning disable RS1024 // Compare symbols correctly + // Bug fixed https://github.com/dotnet/roslyn-analyzers/pull/4571 + private Dictionary ResolvedSymbols = new(); +#pragma warning restore RS1024 // Compare symbols correctly + + BinaryLogger? _binLogger = null; + private BinaryLogger? BinLogger + { + get + { + if (Config.BinLogger) + { + if (_binLogger == null) + { + _binLogger = new BinaryLogger() + { + Parameters = Path.Combine(Environment.CurrentDirectory, Config.BinLogPath), + Verbosity = Microsoft.Build.Framework.LoggerVerbosity.Diagnostic, + CollectProjectImports = BinaryLogger.ProjectImportsCollectionMode.Embed + }; + } + } + + return _binLogger; + } + } public ToTripleSlashPorter(Configuration config) { @@ -27,6 +68,7 @@ public ToTripleSlashPorter(Configuration config) { throw new InvalidOperationException($"Unexpected porting direction: {config.Direction}"); } + Config = config; DocsComments = new DocsCommentsContainer(config); @@ -46,46 +88,83 @@ public void Start() Log.Info("Porting from Docs to triple slash..."); - MSBuildWorkspace workspace; - try - { - workspace = MSBuildWorkspace.Create(); - } - catch (ReflectionTypeLoadException) - { - throw new Exception("The MSBuild directory was not found in PATH. Use '-MSBuild ' to specify it."); - } - - CheckDiagnostics(workspace, "MSBuildWorkspace.Create"); + // Load and store the main project + ProjectDatas.Add(GetProjectData(Config.CsProj!.FullName)); - BinaryLogger? binLogger = null; - if (Config.BinLogger) + foreach (DocsType docsType in DocsComments.Types) { - binLogger = new BinaryLogger() + foreach (ProjectData pd in ProjectDatas) { - Parameters = Path.Combine(Environment.CurrentDirectory, Config.BinLogPath), - Verbosity = Microsoft.Build.Framework.LoggerVerbosity.Diagnostic, - CollectProjectImports = BinaryLogger.ProjectImportsCollectionMode.Embed - }; - } + // Try to find the symbol in the current compilation + INamedTypeSymbol? symbol = + pd.Compilation.GetTypeByMetadataName(docsType.FullName) ?? + pd.Compilation.Assembly.GetTypeByMetadataName(docsType.FullName); + + // If not found, nothing to do - It means that the Docs for APIs + // from an unrelated namespace were loaded for this compilation's assembly + if (symbol == null) + { + Log.Warning($"Type symbol not found in compilation: {docsType.DocId}."); + continue; + } - Project? project = workspace.OpenProjectAsync(Config.CsProj!.FullName, msbuildLogger: binLogger).Result; - if (project == null) - { - throw new Exception("Could not find a project."); + // Make sure at least one syntax tree of this symbol can be found in the current project's compilation + // Otherwise, retrieve the correct project where this symbol is supposed to be found + + Location location = symbol.Locations.FirstOrDefault() + ?? throw new NullReferenceException($"No locations found for {docsType.FullName}."); + + SyntaxTree tree = location.SourceTree + ?? throw new NullReferenceException($"No tree found in the location of {docsType.FullName}."); + + if (pd.Compilation.SyntaxTrees.FirstOrDefault(x => x.FilePath == tree.FilePath) is null) + { + // The symbol has to live in one of the current project's referenced projects + foreach (ProjectReference projectReference in pd.Project.ProjectReferences) + { + PropertyInfo prop = typeof(ProjectId).GetProperty("DebugName", BindingFlags.NonPublic | BindingFlags.Instance) + ?? throw new NullReferenceException("ProjectId.DebugName private property not found."); + + string projectPath = prop.GetValue(projectReference.ProjectId)?.ToString() + ?? throw new NullReferenceException("ProjectId.DebugName value was null."); + + if (string.IsNullOrWhiteSpace(projectPath)) + { + throw new Exception("Project path was empty."); + } + + // Can't reuse the existing Workspace or exception thrown saying we already have the project loaded in this workspace. + // Unfortunately, there is no way to retrieve a references project as a Project instance from the existing workspace. + ProjectData pd2 = GetProjectData(projectPath); + ProjectDatas.Add(pd2); + ResolvedSymbols.Add(symbol, new SymbolData { Api = docsType, ProjectData = pd2 }); + } + } + else + { + ResolvedSymbols.Add(symbol, new SymbolData { Api = docsType, ProjectData = pd }); + } + } } - CheckDiagnostics(workspace, "workspace.OpenProjectAsync"); - Compilation? compilation = project.GetCompilationAsync().Result; - if (compilation == null) + foreach ((ISymbol symbol, SymbolData data) in ResolvedSymbols) { - throw new NullReferenceException("The project's compilation was null."); - } + ProjectData t = data.ProjectData; + foreach (Location location in symbol.Locations) + { + SyntaxTree tree = location.SourceTree + ?? throw new NullReferenceException($"Tree null for {data.Api.FullName}"); - CheckDiagnostics(workspace, "project.GetCompilationAsync"); + SemanticModel model = t.Compilation.GetSemanticModel(tree); + TripleSlashSyntaxRewriter rewriter = new(DocsComments, model, location, location.SourceTree); + SyntaxNode newRoot = rewriter.Visit(tree.GetRoot()) + ?? throw new NullReferenceException($"Returned null root node for {data.Api.FullName} in {tree.FilePath}"); + + File.WriteAllText(tree.FilePath, newRoot.ToFullString()); + } + } - PortCommentsForProject(compilation!); } private void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) @@ -116,46 +195,33 @@ private void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) } } - private void PortCommentsForProject(Compilation compilation) + private ProjectData GetProjectData(string csprojPath) { - foreach (DocsType docsType in DocsComments.Types) + ProjectData t = new ProjectData(); + + try { - INamedTypeSymbol? typeSymbol = - compilation.GetTypeByMetadataName(docsType.FullName) ?? - compilation.Assembly.GetTypeByMetadataName(docsType.FullName); + t.Workspace = MSBuildWorkspace.Create(); + } + catch (ReflectionTypeLoadException) + { + Log.Error("The MSBuild directory was not found in PATH. Use '-MSBuild ' to specify it."); + throw; + } - if (typeSymbol == null) - { - Log.Warning($"Type symbol not found in compilation: {docsType.DocId}"); - continue; - } + CheckDiagnostics(t.Workspace, "MSBuildWorkspace.Create"); - PortCommentsForType(compilation, docsType, typeSymbol); - } - } + t.Project = t.Workspace.OpenProjectAsync(csprojPath, msbuildLogger: BinLogger).Result + ?? throw new NullReferenceException($"Could not find the project: {csprojPath}"); - private void PortCommentsForType(Compilation compilation, IDocsAPI api, ISymbol symbol) - { - foreach (Location location in symbol.Locations) - { - SyntaxTree? tree = location.SourceTree; - if (tree == null) - { - Log.Warning($"Tree not found for location of {symbol.Name}"); - continue; - } + CheckDiagnostics(t.Workspace, $"workspace.OpenProjectAsync - {csprojPath}"); - SemanticModel model = compilation.GetSemanticModel(tree); - var rewriter = new TripleSlashSyntaxRewriter(DocsComments, model, location, tree); - SyntaxNode? newRoot = rewriter.Visit(tree.GetRoot()); - if (newRoot == null) - { - Log.Warning($"New returned root is null for {api.DocId} in {tree.FilePath}"); - continue; - } + t.Compilation = t.Project.GetCompilationAsync().Result + ?? throw new NullReferenceException("The project's compilation was null."); - File.WriteAllText(tree.FilePath, newRoot.ToFullString()); - } + CheckDiagnostics(t.Workspace, $"project.GetCompilationAsync - {csprojPath}"); + + return t; } #region MSBuild loading logic diff --git a/Libraries/XmlHelper.cs b/Libraries/XmlHelper.cs index 198e667..8f33d27 100644 --- a/Libraries/XmlHelper.cs +++ b/Libraries/XmlHelper.cs @@ -91,7 +91,7 @@ public static string GetAttributeValue(XElement parent, string name) } else { - XAttribute attr = parent.Attribute(name); + XAttribute? attr = parent.Attribute(name); if (attr != null) { return attr.Value.Trim(); @@ -114,7 +114,7 @@ public static bool TryGetChildElement(XElement parent, string name, out XElement public static string GetChildElementValue(XElement parent, string childName) { - XElement child = parent.Element(childName); + XElement? child = parent.Element(childName); if (child != null) { diff --git a/Program/DocsPortingTool.cs b/Program/DocsPortingTool.cs index 7aae3f8..3205900 100644 --- a/Program/DocsPortingTool.cs +++ b/Program/DocsPortingTool.cs @@ -13,13 +13,13 @@ public static void Main(string[] args) { case Configuration.PortingDirection.ToDocs: { - var porter = new ToDocsPorter(config); + ToDocsPorter porter = new(config); porter.Start(); break; } case Configuration.PortingDirection.ToTripleSlash: { - var porter = new ToTripleSlashPorter(config); + ToTripleSlashPorter porter = new(config); porter.Start(); break; } diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index 6eb82f8..d8348c5 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -4,11 +4,8 @@ namespace MyNamespace { /// This is the MyType class summary. /// public class MyType { @@ -27,11 +24,8 @@ internal MyType(int myProperty) /// This is the MyProperty summary. /// This is the MyProperty value. /// public int MyProperty { @@ -41,11 +35,8 @@ public int MyProperty /// This is the MyField summary. /// public int MyField = 1; @@ -54,13 +45,9 @@ public int MyProperty /// This is the MyIntMethod param2 summary. /// This is the MyIntMethod return value. It mentions the . /// . - /// /// ]]> /// This is the ArgumentNullException thrown by MyIntMethod. It mentions the . /// This is the IndexOutOfRangeException thrown by MyIntMethod. @@ -71,13 +58,9 @@ public int MyIntMethod(int param1, int param2) /// This is the MyVoidMethod summary. /// . - /// /// ]]> /// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . /// This is the IndexOutOfRangeException thrown by MyVoidMethod. From 9c7fc6ed6a19cb674bddbdd475ca015586041d26 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Sat, 9 Jan 2021 17:35:32 -0800 Subject: [PATCH 51/65] Fix calling the VSInstance loading code before calling the porter constructor. Avoid adding CDATA to remarks. --- .../TripleSlashSyntaxRewriter.cs | 10 +- Libraries/ToTripleSlashPorter.cs | 130 +++++++++--------- Program/DocsPortingTool.cs | 3 +- .../PortToTripleSlashTests.cs | 19 ++- 4 files changed, 86 insertions(+), 76 deletions(-) diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index d8e8aa1..94033fd 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -15,7 +15,7 @@ internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter private DocsCommentsContainer DocsComments { get; } private SemanticModel Model { get; } - public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticModel model, Location location, SyntaxTree tree) : base(visitIntoStructuredTrivia: true) + public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticModel model) : base(visitIntoStructuredTrivia: true) { DocsComments = docsComments; Model = model; @@ -260,10 +260,10 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp if (!text.IsDocsEmpty()) { string trimmedRemarks = text.RemoveSubstrings("").Trim(); // The SyntaxFactory needs to be the one to add these - SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true, remarks: true); - XmlNodeSyntax xmlRemarksContent = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); - XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(xmlRemarksContent); - + //SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true, remarks: true); + //XmlNodeSyntax contents = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); + SyntaxList contents = GetContentsInRows(trimmedRemarks); + XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(contents); return GetXmlTrivia(xmlRemarks, leadingWhitespace); } diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index 8d5da3b..a6be51c 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -32,12 +32,10 @@ private struct SymbolData private readonly Configuration Config; private readonly DocsCommentsContainer DocsComments; - private readonly VisualStudioInstance MSBuildInstance; - private List ProjectDatas = new(); #pragma warning disable RS1024 // Compare symbols correctly // Bug fixed https://github.com/dotnet/roslyn-analyzers/pull/4571 - private Dictionary ResolvedSymbols = new(); + private readonly Dictionary ResolvedSymbols = new(); #pragma warning restore RS1024 // Compare symbols correctly BinaryLogger? _binLogger = null; @@ -62,7 +60,7 @@ private BinaryLogger? BinLogger } } - public ToTripleSlashPorter(Configuration config) + private ToTripleSlashPorter(Configuration config) { if (config.Direction != Configuration.PortingDirection.ToTripleSlash) { @@ -71,14 +69,18 @@ public ToTripleSlashPorter(Configuration config) Config = config; DocsComments = new DocsCommentsContainer(config); + } + + public static void Start(Configuration config) + { + // IMPORTANT: Need to load the MSBuild property before calling the ToTripleSlashPorter constructor. + LoadVSInstance(); - // This ensures we can load MSBuild property before calling the ToTripleSlashPorter constructor - MSBuildInstance = MSBuildLocator.QueryVisualStudioInstances().First(); - Register(MSBuildInstance.MSBuildPath); - MSBuildLocator.RegisterInstance(MSBuildInstance); + ToTripleSlashPorter porter = new ToTripleSlashPorter(config); + porter.Port(); } - public void Start() + private void Port() { DocsComments.CollectFiles(); if (!DocsComments.Types.Any()) @@ -89,62 +91,58 @@ public void Start() Log.Info("Porting from Docs to triple slash..."); // Load and store the main project - ProjectDatas.Add(GetProjectData(Config.CsProj!.FullName)); + ProjectData mainProjectData = GetProjectData(Config.CsProj!.FullName); foreach (DocsType docsType in DocsComments.Types) { - foreach (ProjectData pd in ProjectDatas) + // Try to find the symbol in the current compilation + INamedTypeSymbol? symbol = + mainProjectData.Compilation.GetTypeByMetadataName(docsType.FullName) ?? + mainProjectData.Compilation.Assembly.GetTypeByMetadataName(docsType.FullName); + + // If not found, nothing to do - It means that the Docs for APIs + // from an unrelated namespace were loaded for this compilation's assembly + if (symbol == null) { - // Try to find the symbol in the current compilation - INamedTypeSymbol? symbol = - pd.Compilation.GetTypeByMetadataName(docsType.FullName) ?? - pd.Compilation.Assembly.GetTypeByMetadataName(docsType.FullName); - - // If not found, nothing to do - It means that the Docs for APIs - // from an unrelated namespace were loaded for this compilation's assembly - if (symbol == null) - { - Log.Warning($"Type symbol not found in compilation: {docsType.DocId}."); - continue; - } + Log.Warning($"Type symbol not found in compilation: {docsType.DocId}."); + continue; + } - // Make sure at least one syntax tree of this symbol can be found in the current project's compilation - // Otherwise, retrieve the correct project where this symbol is supposed to be found - - Location location = symbol.Locations.FirstOrDefault() - ?? throw new NullReferenceException($"No locations found for {docsType.FullName}."); + // Make sure at least one syntax tree of this symbol can be found in the current project's compilation + // Otherwise, retrieve the correct project where this symbol is supposed to be found - SyntaxTree tree = location.SourceTree - ?? throw new NullReferenceException($"No tree found in the location of {docsType.FullName}."); + Location location = symbol.Locations.FirstOrDefault() + ?? throw new NullReferenceException($"No locations found for {docsType.FullName}."); + + SyntaxTree tree = location.SourceTree + ?? throw new NullReferenceException($"No tree found in the location of {docsType.FullName}."); - if (pd.Compilation.SyntaxTrees.FirstOrDefault(x => x.FilePath == tree.FilePath) is null) + if (mainProjectData.Compilation.SyntaxTrees.FirstOrDefault(x => x.FilePath == tree.FilePath) is null) + { + // The symbol has to live in one of the current project's referenced projects + foreach (ProjectReference projectReference in mainProjectData.Project.ProjectReferences) { - // The symbol has to live in one of the current project's referenced projects - foreach (ProjectReference projectReference in pd.Project.ProjectReferences) + PropertyInfo prop = typeof(ProjectId).GetProperty("DebugName", BindingFlags.NonPublic | BindingFlags.Instance) + ?? throw new NullReferenceException("ProjectId.DebugName private property not found."); + + string projectPath = prop.GetValue(projectReference.ProjectId)?.ToString() + ?? throw new NullReferenceException("ProjectId.DebugName value was null."); + + if (string.IsNullOrWhiteSpace(projectPath)) { - PropertyInfo prop = typeof(ProjectId).GetProperty("DebugName", BindingFlags.NonPublic | BindingFlags.Instance) - ?? throw new NullReferenceException("ProjectId.DebugName private property not found."); - - string projectPath = prop.GetValue(projectReference.ProjectId)?.ToString() - ?? throw new NullReferenceException("ProjectId.DebugName value was null."); - - if (string.IsNullOrWhiteSpace(projectPath)) - { - throw new Exception("Project path was empty."); - } - - // Can't reuse the existing Workspace or exception thrown saying we already have the project loaded in this workspace. - // Unfortunately, there is no way to retrieve a references project as a Project instance from the existing workspace. - ProjectData pd2 = GetProjectData(projectPath); - ProjectDatas.Add(pd2); - ResolvedSymbols.Add(symbol, new SymbolData { Api = docsType, ProjectData = pd2 }); + throw new Exception("Project path was empty."); } - } - else - { - ResolvedSymbols.Add(symbol, new SymbolData { Api = docsType, ProjectData = pd }); + + // Can't reuse the existing Workspace or exception thrown saying we already have the project loaded in this workspace. + // Unfortunately, there is no way to retrieve a references project as a Project instance from the existing workspace. + ProjectData extraProjectData = GetProjectData(projectPath); + ResolvedSymbols.Add(symbol, new SymbolData { Api = docsType, ProjectData = extraProjectData }); } } + else + { + ResolvedSymbols.Add(symbol, new SymbolData { Api = docsType, ProjectData = mainProjectData }); + } } @@ -157,14 +155,13 @@ public void Start() ?? throw new NullReferenceException($"Tree null for {data.Api.FullName}"); SemanticModel model = t.Compilation.GetSemanticModel(tree); - TripleSlashSyntaxRewriter rewriter = new(DocsComments, model, location, location.SourceTree); + TripleSlashSyntaxRewriter rewriter = new(DocsComments, model); SyntaxNode newRoot = rewriter.Visit(tree.GetRoot()) ?? throw new NullReferenceException($"Returned null root node for {data.Api.FullName} in {tree.FilePath}"); File.WriteAllText(tree.FilePath, newRoot.ToFullString()); } } - } private void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) @@ -197,7 +194,7 @@ private void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) private ProjectData GetProjectData(string csprojPath) { - ProjectData t = new ProjectData(); + ProjectData t = new(); try { @@ -224,16 +221,24 @@ private ProjectData GetProjectData(string csprojPath) return t; } + #region MSBuild loading logic - private static readonly Dictionary s_pathsToAssemblies = new Dictionary(StringComparer.OrdinalIgnoreCase); - private static readonly Dictionary s_namesToAssemblies = new Dictionary(); + private static readonly Dictionary s_pathsToAssemblies = new(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary s_namesToAssemblies = new(); - private static readonly object s_guard = new object(); + private static readonly object s_guard = new(); - /// - /// Register an assembly loader that will load assemblies with higher version than what was requested. - /// + // Loads the external VS instance using the correct MSBuild dependency, which differs from the one used by this process. + public static VisualStudioInstance LoadVSInstance() + { + VisualStudioInstance vsBuildInstance = MSBuildLocator.QueryVisualStudioInstances().First(); + Register(vsBuildInstance.MSBuildPath); + MSBuildLocator.RegisterInstance(vsBuildInstance); + return vsBuildInstance; + } + + // Register an assembly loader that will load assemblies with higher version than what was requested. private static void Register(string searchPath) { AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext context, AssemblyName assemblyName) => @@ -314,6 +319,5 @@ private static void Register(string searchPath) } #endregion - } } diff --git a/Program/DocsPortingTool.cs b/Program/DocsPortingTool.cs index 3205900..828b39b 100644 --- a/Program/DocsPortingTool.cs +++ b/Program/DocsPortingTool.cs @@ -19,8 +19,7 @@ public static void Main(string[] args) } case Configuration.PortingDirection.ToTripleSlash: { - ToTripleSlashPorter porter = new(config); - porter.Start(); + ToTripleSlashPorter.Start(config); break; } default: diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs index 0b188ca..884af43 100644 --- a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs +++ b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs @@ -1,4 +1,11 @@ -using System.IO; +#nullable enable +using Microsoft.Build.Locator; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; using Xunit; namespace Libraries.Tests @@ -11,7 +18,7 @@ public void Port_Basic() PortToTripleSlash("Basic"); } - private void PortToTripleSlash( + private static void PortToTripleSlash( string testDataDir, bool save = true, bool skipInterfaceImplementations = true, @@ -19,9 +26,9 @@ private void PortToTripleSlash( string namespaceName = TestData.TestNamespace, string typeName = TestData.TestType) { - using TestDirectory tempDir = new TestDirectory(); + using TestDirectory tempDir = new(); - PortToTripleSlashTestData testData = new PortToTripleSlashTestData( + PortToTripleSlashTestData testData = new( tempDir, testDataDir, assemblyName: assemblyName, @@ -45,13 +52,13 @@ private void PortToTripleSlash( c.DirsDocsXml.Add(testData.DocsDir); - var porter = new ToTripleSlashPorter(c); + var porter = new ToTripleSlashPorter(c, ToTripleSlashPorter.LoadVSInstance()); porter.Start(); Verify(testData); } - private void Verify(PortToTripleSlashTestData testData) + private static void Verify(PortToTripleSlashTestData testData) { string[] expectedLines = File.ReadAllLines(testData.ExpectedFilePath); string[] actualLines = File.ReadAllLines(testData.ActualFilePath); From 701ef4463031d68d2321e5c08aa29e71fc051d63 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 15 Jan 2021 10:08:26 -0800 Subject: [PATCH 52/65] Address test PR suggestions. --- Libraries/Extensions.cs | 6 +- .../TripleSlashSyntaxRewriter.cs | 399 +++++++++++++----- Libraries/ToTripleSlashPorter.cs | 58 ++- Program/DocsPortingTool.csproj | 1 + Program/Properties/launchSettings.json | 4 +- .../PortToTripleSlashTests.cs | 3 +- .../TestData/Basic/MyDelegate.xml | 12 +- .../TestData/Basic/MyType.xml | 76 ++-- .../TestData/Basic/SourceExpected.cs | 82 ++-- .../TestData/Basic/SourceOriginal.cs | 44 +- 10 files changed, 475 insertions(+), 210 deletions(-) diff --git a/Libraries/Extensions.cs b/Libraries/Extensions.cs index 56c6a7b..40a5d63 100644 --- a/Libraries/Extensions.cs +++ b/Libraries/Extensions.cs @@ -39,7 +39,7 @@ public static string RemoveSubstrings(this string oldString, params string[] str public static bool IsDocsEmpty(this string? s) => string.IsNullOrWhiteSpace(s) || s == Configuration.ToBeAdded; - public static string WithoutPrefix(this string text) + public static string WithoutDocIdPrefixes(this string text) { if (text.Length > 2 && text[1] == ':') { @@ -48,8 +48,8 @@ public static string WithoutPrefix(this string text) return Regex.Replace( input: text, - pattern: @"(?.*)(?cref=""[A-Z]\:)(?.*)", - replacement: "${left}cref=\"${right}"); + pattern: @"cref=""[a-zA-Z]{1}\:(?[a-zA-Z0-9\._]+)""", + replacement: "cref=\"${cref}\""); } } diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index 94033fd..86d59c2 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -7,9 +7,85 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text.RegularExpressions; namespace Libraries.RoslynTripleSlash { + /* + The following triple slash comments section: + + /// + /// My summary. + /// + /// My param description. + /// My remarks. + public ... + + translates to this syntax tree structure: + + 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. + + 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 + + XmlElement -> My remarks. + XmlText -> new line char after + XmlTextLiteralNewLineToken -> new line char after + EndOfDocumentationCommentToken (SyntaxToken) -> invisible + + Lead: WhitespaceTrivia -> The 4 whitespace chars before the public keyword. + Trail: WhitespaceTrivia -> The single whitespace char after the public keyword. + */ internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter { private DocsCommentsContainer DocsComments { get; } @@ -83,6 +159,9 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) => VisitBaseMethodDeclaration(node); + public override SyntaxNode? VisitOperatorDeclaration(OperatorDeclarationSyntax node) => + VisitBaseMethodDeclaration(node); + public override SyntaxNode? VisitPropertyDeclaration(PropertyDeclarationSyntax node) { if (!TryGetMember(node, out DocsMember? member)) @@ -92,9 +171,9 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); - SyntaxTriviaList value = GetValue(member.Value, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member, leadingWhitespace); + SyntaxTriviaList value = GetValue(member, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member, leadingWhitespace); SyntaxTriviaList exceptions = GetExceptions(member.Exceptions, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); @@ -103,6 +182,20 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod return GetNodeWithTrivia(leadingWhitespace, node, summary, value, remarks, exceptions, seealsos, altmembers, relateds); } + public override SyntaxNode? VisitRecordDeclaration(RecordDeclarationSyntax node) + { + SyntaxNode? baseNode = base.VisitRecordDeclaration(node); + + ISymbol? symbol = Model.GetDeclaredSymbol(node); + if (symbol == null) + { + Log.Warning($"Symbol is null."); + return baseNode; + } + + return VisitType(baseNode, symbol); + } + public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node) { SyntaxNode? baseNode = base.VisitStructDeclaration(node); @@ -141,8 +234,8 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod } - SyntaxTriviaList summary = GetSummary(type.Summary, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(type.Remarks, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(type, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(type, leadingWhitespace); SyntaxTriviaList parameters = GetParameters(type, leadingWhitespace); SyntaxTriviaList typeParameters = GetTypeParameters(type, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(type.SeeAlsoCrefs, leadingWhitespace); @@ -164,11 +257,11 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member, leadingWhitespace); SyntaxTriviaList parameters = GetParameters(member, leadingWhitespace); SyntaxTriviaList typeParameters = GetTypeParameters(member, leadingWhitespace); - SyntaxTriviaList returns = GetReturns(member.Returns, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList returns = GetReturns(member, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member, leadingWhitespace); SyntaxTriviaList exceptions = GetExceptions(member.Exceptions, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); @@ -186,8 +279,8 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member, leadingWhitespace); SyntaxTriviaList exceptions = GetExceptions(member.Exceptions, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); @@ -211,8 +304,8 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node); - SyntaxTriviaList summary = GetSummary(member.Summary, leadingWhitespace); - SyntaxTriviaList remarks = GetRemarks(member.Remarks, leadingWhitespace); + SyntaxTriviaList summary = GetSummary(member, leadingWhitespace); + SyntaxTriviaList remarks = GetRemarks(member, leadingWhitespace); SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace); SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace); SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace); @@ -223,46 +316,69 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod return node; } - private SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxNode node, params SyntaxTriviaList[] trivias) + private static SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxNode node, params SyntaxTriviaList[] trivias) { SyntaxTriviaList finalTrivia = new(); - var leadingTrivia = node.GetLeadingTrivia(); - if (leadingTrivia.Any()) + foreach (SyntaxTriviaList t in trivias) + { + finalTrivia = finalTrivia.AddRange(t); + } + if (finalTrivia.Count > 0) { - if (leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) + finalTrivia = finalTrivia.AddRange(leadingWhitespace); + + var leadingTrivia = node.GetLeadingTrivia(); + if (leadingTrivia.Any()) { - // Ensure the endline that separates nodes is respected - finalTrivia = new(SyntaxFactory.ElasticCarriageReturnLineFeed); + if (leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) + { + // Ensure the endline that separates nodes is respected + finalTrivia = new SyntaxTriviaList(SyntaxFactory.ElasticCarriageReturnLineFeed) + .AddRange(finalTrivia); + } } - } - foreach (SyntaxTriviaList t in trivias) - { - finalTrivia = finalTrivia.AddRange(t); + return node.WithLeadingTrivia(finalTrivia); } - finalTrivia = finalTrivia.AddRange(leadingWhitespace); - return node.WithLeadingTrivia(finalTrivia); + // If there was no new trivia, return untouched + return node; } - private SyntaxTriviaList GetLeadingWhitespace(SyntaxNode node) => - node.GetLeadingTrivia().Where(t => t.IsKind(SyntaxKind.WhitespaceTrivia)).ToSyntaxTriviaList(); + // 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) + { + if (node is MemberDeclarationSyntax memberDeclaration) + { + if (memberDeclaration.Modifiers.FirstOrDefault(x => x.IsKind(SyntaxKind.PublicKeyword) || x.IsKind(SyntaxKind.ProtectedKeyword)) is SyntaxToken publicModifier) + { + if (publicModifier.LeadingTrivia.LastOrDefault(t => t.IsKind(SyntaxKind.WhitespaceTrivia)) is SyntaxTrivia last) + { + return new(last); + } + } + } + return new(); + } - private SyntaxTriviaList GetSummary(string text, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetSummary(DocsAPI api, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); - XmlElementSyntax element = SyntaxFactory.XmlSummaryElement(contents); - return GetXmlTrivia(element, leadingWhitespace); + if (!api.Summary.IsDocsEmpty()) + { + XmlTextSyntax contents = GetTextAsCommentedTokens(api.Summary, leadingWhitespace); + XmlElementSyntax element = SyntaxFactory.XmlSummaryElement(contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + return new(); } - private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetRemarks(DocsAPI api, SyntaxTriviaList leadingWhitespace) { - if (!text.IsDocsEmpty()) + if (!api.Remarks.IsDocsEmpty()) { - string trimmedRemarks = text.RemoveSubstrings("").Trim(); // The SyntaxFactory needs to be the one to add these - //SyntaxTokenList cdata = GetTextAsTokens(trimmedRemarks, leadingWhitespace.Add(SyntaxFactory.CarriageReturnLineFeed), addInitialNewLine: true, remarks: true); - //XmlNodeSyntax contents = SyntaxFactory.XmlCDataSection(SyntaxFactory.Token(SyntaxKind.XmlCDataStartToken), cdata, SyntaxFactory.Token(SyntaxKind.XmlCDataEndToken)); - SyntaxList contents = GetContentsInRows(trimmedRemarks); + string text = GetRemarksWithXmlParameters(api); + XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace, markdown: true); XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(contents); return GetXmlTrivia(xmlRemarks, leadingWhitespace); } @@ -270,74 +386,123 @@ private SyntaxTriviaList GetRemarks(string text, SyntaxTriviaList leadingWhitesp return new(); } - private SyntaxTriviaList GetValue(string text, SyntaxTriviaList leadingWhitespace) + private static string GetRemarksWithXmlParameters(IDocsAPI api) { - SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); - XmlElementSyntax element = SyntaxFactory.XmlValueElement(contents); - return GetXmlTrivia(element, leadingWhitespace); + string remarks = api.Remarks; + + if (!api.Remarks.IsDocsEmpty() && ( + api.Params.Any() || api.TypeParams.Any())) + { + MatchCollection collection = Regex.Matches(api.Remarks, @"(?`(?[a-zA-Z0-9_]+)`)"); + + foreach (Match match in collection) + { + string backtickedParam = match.Groups["backtickedParam"].Value; + string paramName = match.Groups["paramName"].Value; + if (api.Params.Any(x => x.Name == paramName)) + { + remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); + } + else if (api.TypeParams.Any(x => x.Name == paramName)) + { + remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); + } + } + } + + return remarks; } - private SyntaxTriviaList GetParameter(string name, string text, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetValue(DocsMember api, SyntaxTriviaList leadingWhitespace) { - SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); - XmlElementSyntax element = SyntaxFactory.XmlParamElement(name, contents); - return GetXmlTrivia(element, leadingWhitespace); + if (!api.Value.IsDocsEmpty()) + { + XmlTextSyntax contents = GetTextAsCommentedTokens(api.Value, leadingWhitespace); + XmlElementSyntax element = SyntaxFactory.XmlValueElement(contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + return new(); + } + + 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(element, leadingWhitespace); + } + + return new(); } - private SyntaxTriviaList GetParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList parameters = new(); - foreach (SyntaxTriviaList parameterTrivia in api.Params.Select( - param => GetParameter(param.Name, param.Value, leadingWhitespace))) + 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 SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace) { - var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute("name", name)); - SyntaxList contents = GetContentsInRows(text); - return GetXmlTrivia("typeparam", attribute, contents, 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 SyntaxTriviaList GetTypeParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetTypeParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList typeParameters = new(); - foreach (SyntaxTriviaList typeParameterTrivia in api.TypeParams.Select( - typeParam => GetTypeParam(typeParam.Name, typeParam.Value, leadingWhitespace))) + 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 SyntaxTriviaList GetReturns(string text, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetReturns(DocsMember api, SyntaxTriviaList leadingWhitespace) { - // For when returns is empty because the method returns void - if (string.IsNullOrWhiteSpace(text)) + // Also applies for when is empty because the method return type is void + if (!api.Returns.IsDocsEmpty()) { - return new(); + XmlTextSyntax contents = GetTextAsCommentedTokens(api.Returns, leadingWhitespace); + XmlElementSyntax element = SyntaxFactory.XmlReturnsElement(contents); + return GetXmlTrivia(element, leadingWhitespace); } - SyntaxList contents = GetContentsInRows(text.WithoutPrefix()); - XmlElementSyntax element = SyntaxFactory.XmlReturnsElement(contents); - return GetXmlTrivia(element, leadingWhitespace); + return new(); } - private SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace) { - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutPrefix())); - XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text.WithoutPrefix(), leadingWhitespace, addInitialNewLine: false)); - XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); - return GetXmlTrivia(element, leadingWhitespace); + if (!text.IsDocsEmpty()) + { + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutDocIdPrefixes())); + //XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text.WithoutPrefix(), leadingWhitespace)); + XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace); + XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); + return GetXmlTrivia(element, leadingWhitespace); + } + + return new(); } - private SyntaxTriviaList GetExceptions(List docsExceptions, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetExceptions(List docsExceptions, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList exceptions = new(); - // No need to add exceptions in secondary files if (docsExceptions.Any()) { foreach (SyntaxTriviaList exceptionsTrivia in docsExceptions.Select( @@ -349,14 +514,14 @@ private SyntaxTriviaList GetExceptions(List docsExceptions, Synta return exceptions; } - private SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace) { - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutPrefix())); + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutDocIdPrefixes())); XmlEmptyElementSyntax element = SyntaxFactory.XmlSeeAlsoElement(crefSyntax); return GetXmlTrivia(element, leadingWhitespace); } - private SyntaxTriviaList GetSeeAlsos(List docsSeeAlsoCrefs, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetSeeAlsos(List docsSeeAlsoCrefs, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList seealsos = new(); if (docsSeeAlsoCrefs.Any()) @@ -370,14 +535,14 @@ private SyntaxTriviaList GetSeeAlsos(List docsSeeAlsoCrefs, SyntaxTrivia return seealsos; } - private SyntaxTriviaList GetAltMember(string cref, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetAltMember(string cref, SyntaxTriviaList leadingWhitespace) { - XmlAttributeSyntax attribute = SyntaxFactory.XmlTextAttribute("cref", cref.WithoutPrefix()); + XmlAttributeSyntax attribute = SyntaxFactory.XmlTextAttribute("cref", cref.WithoutDocIdPrefixes()); XmlEmptyElementSyntax emptyElement = SyntaxFactory.XmlEmptyElement(SyntaxFactory.XmlName(SyntaxFactory.Identifier("altmember")), new SyntaxList(attribute)); return GetXmlTrivia(emptyElement, leadingWhitespace); } - private SyntaxTriviaList GetAltMembers(List docsAltMembers, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetAltMembers(List docsAltMembers, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList altMembers = new(); if (docsAltMembers.Any()) @@ -391,18 +556,18 @@ private SyntaxTriviaList GetAltMembers(List docsAltMembers, SyntaxTrivia return altMembers; } - private SyntaxTriviaList GetRelated(string articleType, string href, string value, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetRelated(string articleType, string href, string value, SyntaxTriviaList leadingWhitespace) { SyntaxList attributes = new(); attributes = attributes.Add(SyntaxFactory.XmlTextAttribute("type", articleType)); attributes = attributes.Add(SyntaxFactory.XmlTextAttribute("href", href)); - SyntaxList contents = GetContentsInRows(value); + XmlTextSyntax contents = GetTextAsCommentedTokens(value, leadingWhitespace); return GetXmlTrivia("related", attributes, contents, leadingWhitespace); } - private SyntaxTriviaList GetRelateds(List docsRelateds, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetRelateds(List docsRelateds, SyntaxTriviaList leadingWhitespace) { SyntaxTriviaList relateds = new(); if (docsRelateds.Any()) @@ -416,61 +581,62 @@ private SyntaxTriviaList GetRelateds(List docsRelateds, SyntaxTrivi return relateds; } - private SyntaxTokenList GetTextAsTokens(string text, SyntaxTriviaList leadingWhitespace, bool addInitialNewLine, bool remarks = false) + private static string ReplaceText(string text, bool markdown) + { + if (markdown) + { + text = Regex.Replace(text, @"", ""); + text = Regex.Replace(text, @"##[ ]?Remarks(\r?\n)*[\t ]*", ""); + text = Regex.Replace(text, @"(?[a-zA-Z0-9_\.]+)>)", ""); + } + else + { + text = text.WithoutDocIdPrefixes(); + } + + return text; + } + + /* + XmlText + XmlTextLiteralNewLineToken (XmlTextSyntax) -> endline + XmlTextLiteralToken (XmlTextLiteralToken) -> [ text] + Lead: DocumentationCommentExteriorTrivia (SyntaxTrivia) -> [ /// ] + */ + private static XmlTextSyntax GetTextAsCommentedTokens(string text, SyntaxTriviaList leadingWhitespace, bool markdown = false) { - string whitespace = leadingWhitespace.ToFullString().Replace(Environment.NewLine, ""); - SyntaxToken newLineAndWhitespace = SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace); + text = ReplaceText(text, markdown); + + // collapse newlines to a single one + string whitespace = Regex.Replace(leadingWhitespace.ToFullString(), @"(\r?\n)+", ""); + SyntaxToken whitespaceToken = SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace); SyntaxTrivia leadingTrivia = SyntaxFactory.SyntaxTrivia(SyntaxKind.DocumentationCommentExteriorTrivia, string.Empty); SyntaxTriviaList leading = SyntaxTriviaList.Create(leadingTrivia); - + var tokens = new List(); - string[] splittedLines = text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + string[] lines = text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - // Only add the initial new line and whitespace if the contents have more than one line. Otherwise, we want the contents to be inlined inside the tags. - if (splittedLines.Length > 1 && addInitialNewLine) + for (int lineNumber = 0; lineNumber < lines.Length; lineNumber++) { - tokens.Add(newLineAndWhitespace); - } - - for (int lineNumber = 0; lineNumber < splittedLines.Length; lineNumber++) - { - string line = splittedLines[lineNumber]; - - // Avoid adding the '## Remarks' header, it's unnecessary - if (remarks && (line.Contains("## Remarks") || line.Contains("##Remarks"))) - { - // Reduces the number of Contains calls - remarks = false; - continue; - } + string line = lines[lineNumber]; SyntaxToken token = SyntaxFactory.XmlTextLiteral(leading, line, line, default); tokens.Add(token); - // Only add extra new lines if we expect more than one line of text in the contents. Otherwise, inline it inside the tags. - if (splittedLines.Length > 1) + if (lines.Length > 1 && lineNumber < lines.Length - 1) { - tokens.Add(newLineAndWhitespace); + tokens.Add(whitespaceToken); } } - return SyntaxFactory.TokenList(tokens); - } - private SyntaxList GetContentsInRows(string text) - { - var nodes = new SyntaxList(); - foreach (string line in text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) - { - var tokenList = SyntaxFactory.ParseTokens(line).ToArray(); // Prevents unexpected change from "<" to "<" - XmlTextSyntax xmlText = SyntaxFactory.XmlText(tokenList); - return nodes.Add(xmlText); - } - return nodes; + XmlTextSyntax xmlText = SyntaxFactory.XmlText(tokens.ToArray()); + return xmlText; } - private SyntaxTriviaList GetXmlTrivia(XmlNodeSyntax node, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetXmlTrivia(XmlNodeSyntax node, SyntaxTriviaList leadingWhitespace) { DocumentationCommentTriviaSyntax docComment = SyntaxFactory.DocumentationComment(node); SyntaxTrivia docCommentTrivia = SyntaxFactory.Trivia(docComment); @@ -483,7 +649,7 @@ private SyntaxTriviaList GetXmlTrivia(XmlNodeSyntax node, SyntaxTriviaList leadi // Generates a custom SyntaxTrivia object containing a triple slashed xml element with optional attributes. // Looks like below (excluding square brackets): // [ /// text] - private SyntaxTriviaList GetXmlTrivia(string name, SyntaxList attributes, SyntaxList contents, SyntaxTriviaList leadingWhitespace) + private static SyntaxTriviaList GetXmlTrivia(string name, SyntaxList attributes, XmlTextSyntax contents, SyntaxTriviaList leadingWhitespace) { XmlElementStartTagSyntax start = SyntaxFactory.XmlElementStartTag( SyntaxFactory.Token(SyntaxKind.LessThanToken), @@ -496,8 +662,7 @@ private SyntaxTriviaList GetXmlTrivia(string name, SyntaxList(contents), end); return GetXmlTrivia(element, leadingWhitespace); } diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs index a6be51c..526275a 100644 --- a/Libraries/ToTripleSlashPorter.cs +++ b/Libraries/ToTripleSlashPorter.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -35,7 +36,7 @@ private struct SymbolData #pragma warning disable RS1024 // Compare symbols correctly // Bug fixed https://github.com/dotnet/roslyn-analyzers/pull/4571 - private readonly Dictionary ResolvedSymbols = new(); + private readonly Dictionary ResolvedSymbols = new(); #pragma warning restore RS1024 // Compare symbols correctly BinaryLogger? _binLogger = null; @@ -76,7 +77,7 @@ public static void Start(Configuration config) // IMPORTANT: Need to load the MSBuild property before calling the ToTripleSlashPorter constructor. LoadVSInstance(); - ToTripleSlashPorter porter = new ToTripleSlashPorter(config); + var porter = new ToTripleSlashPorter(config); porter.Port(); } @@ -114,8 +115,12 @@ private void Port() Location location = symbol.Locations.FirstOrDefault() ?? throw new NullReferenceException($"No locations found for {docsType.FullName}."); - SyntaxTree tree = location.SourceTree - ?? throw new NullReferenceException($"No tree found in the location of {docsType.FullName}."); + SyntaxTree? tree = location.SourceTree; + if (tree == null) + { + Log.Warning($"No tree found in the location of {docsType.FullName}. Skipping."); + continue; + } if (mainProjectData.Compilation.SyntaxTrees.FirstOrDefault(x => x.FilePath == tree.FilePath) is null) { @@ -135,8 +140,9 @@ private void Port() // Can't reuse the existing Workspace or exception thrown saying we already have the project loaded in this workspace. // Unfortunately, there is no way to retrieve a references project as a Project instance from the existing workspace. - ProjectData extraProjectData = GetProjectData(projectPath); - ResolvedSymbols.Add(symbol, new SymbolData { Api = docsType, ProjectData = extraProjectData }); + ProjectData extraProjectData = GetProjectDataAndSymbol(projectPath, docsType.FullName, out INamedTypeSymbol? actualSymbol); + + ResolvedSymbols.Add(actualSymbol, new SymbolData { Api = docsType, ProjectData = extraProjectData }); } } else @@ -164,7 +170,7 @@ private void Port() } } - private void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) + private static void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) { ImmutableList diagnostics = workspace.Diagnostics; if (diagnostics.Any()) @@ -192,13 +198,34 @@ private void CheckDiagnostics(MSBuildWorkspace workspace, string stepName) } } + private ProjectData GetProjectDataAndSymbol( + string csprojPath, + string symbolFullName, + [NotNull] out INamedTypeSymbol? actualSymbol) + { + ProjectData pd = GetProjectData(csprojPath); + + // Try to find the symbol in the current compilation + actualSymbol = + pd.Compilation.GetTypeByMetadataName(symbolFullName) ?? + pd.Compilation.Assembly.GetTypeByMetadataName(symbolFullName); + + if (actualSymbol == null) + { + Log.Error($"Type symbol not found in compilation: {symbolFullName}."); + throw new NullReferenceException(); + } + + return pd; + } + private ProjectData GetProjectData(string csprojPath) { - ProjectData t = new(); + ProjectData pd = new(); try { - t.Workspace = MSBuildWorkspace.Create(); + pd.Workspace = MSBuildWorkspace.Create(); } catch (ReflectionTypeLoadException) { @@ -206,22 +233,21 @@ private ProjectData GetProjectData(string csprojPath) throw; } - CheckDiagnostics(t.Workspace, "MSBuildWorkspace.Create"); + CheckDiagnostics(pd.Workspace, "MSBuildWorkspace.Create"); - t.Project = t.Workspace.OpenProjectAsync(csprojPath, msbuildLogger: BinLogger).Result + pd.Project = pd.Workspace.OpenProjectAsync(csprojPath, msbuildLogger: BinLogger).Result ?? throw new NullReferenceException($"Could not find the project: {csprojPath}"); - CheckDiagnostics(t.Workspace, $"workspace.OpenProjectAsync - {csprojPath}"); + CheckDiagnostics(pd.Workspace, $"workspace.OpenProjectAsync - {csprojPath}"); - t.Compilation = t.Project.GetCompilationAsync().Result + pd.Compilation = pd.Project.GetCompilationAsync().Result ?? throw new NullReferenceException("The project's compilation was null."); - CheckDiagnostics(t.Workspace, $"project.GetCompilationAsync - {csprojPath}"); + CheckDiagnostics(pd.Workspace, $"project.GetCompilationAsync - {csprojPath}"); - return t; + return pd; } - #region MSBuild loading logic private static readonly Dictionary s_pathsToAssemblies = new(StringComparer.OrdinalIgnoreCase); diff --git a/Program/DocsPortingTool.csproj b/Program/DocsPortingTool.csproj index 6efdea0..47e19b5 100644 --- a/Program/DocsPortingTool.csproj +++ b/Program/DocsPortingTool.csproj @@ -10,6 +10,7 @@ true true 3.0.0 + true diff --git a/Program/Properties/launchSettings.json b/Program/Properties/launchSettings.json index 35956d8..80b814e 100644 --- a/Program/Properties/launchSettings.json +++ b/Program/Properties/launchSettings.json @@ -2,12 +2,14 @@ "profiles": { "Program": { "commandName": "Project", - "commandLineArgs": "-CsProj D:\\runtime\\src\\libraries\\System.IO.Compression\\src\\System.IO.Compression.csproj -Docs D:\\dotnet-api-docs\\xml -Save true -Direction ToTripleSlash -IncludedAssemblies System.IO.Compression,System.IO.Compression.Brotli -SkipInterfaceImplementations true", + "commandLineArgs": "-CsProj D:\\runtime\\src\\libraries\\System.IO.Compression.Brotli\\src\\System.IO.Compression.Brotli.csproj -Docs D:\\dotnet-api-docs\\xml -Save true -Direction ToTripleSlash -IncludedAssemblies System.IO.Compression.Brotli -IncludedNamespaces System.IO.Compression", "environmentVariables": { + "S.Numerics.Vectors": "-CsProj D:\\runtime\\src\\libraries\\System.Numerics.Vectors\\src\\System.Numerics.Vectors.csproj -Docs D:\\dotnet-api-docs\\xml -Save true -Direction ToTripleSlash -IncludedAssemblies System.Numerics.Vectors -IncludedNamespaces System.Numerics,System.Numerics.Vectors", "DOCS_IOT": "D:\\iot\\artifacts\\bin", "DOCS_CORECLR": "D:\\runtime\\artifacts\\bin\\coreclr\\Windows_NT.x64.Release\\IL\\", "DOCS_WINFORMS": "D:\\winforms\\artifacts\\bin\\", "DOCS_WPF": "D:\\wpf\\.tools\\native\\bin\\dotnet-api-docs_netcoreapp3.0\\0.0.0.1\\_intellisense\\\\netcore-3.0\\", + "S.IO.C.Brotli": "-CsProj D:\\runtime\\src\\libraries\\System.IO.Compression.Brotli\\src\\System.IO.Compression.Brotli.csproj -Docs D:\\dotnet-api-docs\\xml -Save true -Direction ToTripleSlash -IncludedAssemblies System.IO.Compression.Brotli -IncludedNamespaces System.IO.Compression", "DOCS_RUNTIME": "D:\\runtime\\artifacts\\bin\\" } } diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs index 884af43..4170247 100644 --- a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs +++ b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs @@ -52,8 +52,7 @@ private static void PortToTripleSlash( c.DirsDocsXml.Add(testData.DocsDir); - var porter = new ToTripleSlashPorter(c, ToTripleSlashPorter.LoadVSInstance()); - porter.Start(); + ToTripleSlashPorter.Start(c); Verify(testData); } diff --git a/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml b/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml index 42af915..2f13585 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml +++ b/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml @@ -1,18 +1,12 @@ - - + + MyAssembly - - - - - - System.Void - This is the sender parameter. This is the e parameter. + This is the MyDelegate typeparam T. This is the MyDelegate summary. To be added. diff --git a/Tests/PortToTripleSlash/TestData/Basic/MyType.xml b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml index c40b945..61abfeb 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/MyType.xml +++ b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml @@ -32,13 +32,9 @@ Multiple lines. Property - MyAssembly - - System.Int32 - This is the MyProperty summary. This is the MyProperty value. @@ -49,7 +45,7 @@ Multiple lines. These are the MyProperty remarks. -Multiple lines. +Multiple lines and a reference to the field . ]]> @@ -61,9 +57,6 @@ Multiple lines. MyAssembly - - System.Int32 - 1 This is the MyField summary. @@ -83,13 +76,9 @@ Multiple lines. Method - MyAssembly - - System.Int32 - This is the MyIntMethod param1 summary. This is the MyIntMethod param2 summary. @@ -104,7 +93,7 @@ These are the MyIntMethod remarks. Multiple lines. -Mentions the `param1` and the . +Mentions the `param1`, the and the `param2`. ]]> @@ -115,14 +104,9 @@ Mentions the `param1` and the . Method - MyAssembly - 4.0.0.0 - - System.Void - This is the MyVoidMethod summary. @@ -139,23 +123,47 @@ Mentions the . ]]> This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . - This is the IndexOutOfRangeException thrown by MyVoidMethod. + This is the IndexOutOfRangeException thrown by MyVoidMethod. + +-or- + +This is the second case. + +Empty newlines should be respected. + + + + + Method + + MyAssembly + + + To be added. + To be added. - + Method - MyAssembly - - System.Void - This is the MyTypeParamMethod typeparam T. + This is the MyTypeParamMethod parameter param1. This is the MyTypeParamMethod summary. - To be added. + + + @@ -164,11 +172,23 @@ Mentions the . MyAssembly - - MyNamespace.MyDelegate - This is the MyEvent summary. + To be added. + + + + + Method + + MyAssembly + + + The first type to add. + The second type to add. + Adds two MyType instances. + The added types. + To be added. diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index d8348c5..b724ff5 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -3,86 +3,112 @@ namespace MyNamespace { /// This is the MyType class summary. - /// + /// These are the MyType class remarks. + /// Multiple lines. public class MyType { /// This is the MyType constructor summary. public MyType() { - } + } /* Trailing comments should remain untouched */ + // Original double slash comments. They should not be replaced (internal). internal MyType(int myProperty) { _myProperty = myProperty; - } + } // Trailing comments should remain untouched + + /// + /// Triple slash comments above private members should remain untouched. + /// + private int _otherProperty; + // Double slash comments above private members should remain untouched. private int _myProperty; /// This is the MyProperty summary. /// This is the MyProperty value. - /// + /// These are the MyProperty remarks. + /// Multiple lines and a reference to the field . public int MyProperty { - get { return _myProperty; } - set { _myProperty = value; } + get { return _myProperty; /* Internal comments should remain untouched. */ } + set { _myProperty = value; } // Internal comments should remain untouched } /// This is the MyField summary. - /// + /// These are the MyField remarks. + /// Multiple lines. public int MyField = 1; /// 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 . - /// These are the MyIntMethod remarks. /// Multiple lines. - /// Mentions the `param1` and the . - /// ]]> + /// Mentions the , the and the . /// This is the ArgumentNullException thrown by MyIntMethod. It mentions the . /// This is the IndexOutOfRangeException thrown by MyIntMethod. public int MyIntMethod(int param1, int param2) { + // Internal comments should remain untouched. return MyField + param1 + param2; } /// This is the MyVoidMethod summary. - /// These are the MyVoidMethod remarks. /// Multiple lines. - /// Mentions the . - /// ]]> + /// Mentions the . /// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . - /// This is the IndexOutOfRangeException thrown by MyVoidMethod. + /// This is the IndexOutOfRangeException thrown by MyVoidMethod. + /// -or- + /// This is the second case. + /// Empty newlines should be respected. public void MyVoidMethod() { } + /// + /// This method simulates a newly added API that did not have documentation in the docs xml. + /// The developer added the documentation in triple slash comments, so they should be preserved + /// and considered the source of truth. + /// + /// + /// These remarks are the source of truth. + /// + public void UndocumentedMethod() + { + } + /// This is the MyTypeParamMethod summary. + /// This is the MyTypeParamMethod parameter param1. /// This is the MyTypeParamMethod typeparam T. - public void MyTypeParamMethod() + /// This is a reference to the typeparam . + /// This is a reference to the parameter . + public void MyTypeParamMethod(int param1) { } /// This is the MyDelegate summary. /// This is the sender parameter. /// This is the e parameter. + /// This is the MyDelegate typeparam T. /// /// /// The .NET Runtime repo. - public delegate void MyDelegate(object sender, object e); + public delegate void MyDelegate(object sender, T e); /// This is the MyEvent summary. public event MyDelegate MyEvent; + + /// Adds two MyType instances. + /// The first type to add. + /// The second type to add. + /// The added types. + public static MyType operator +(MyType value1, MyType value2) + { + return value1; + } } -} +} \ No newline at end of file diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs index 2993f5e..cb1a1c9 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs @@ -4,27 +4,42 @@ namespace MyNamespace { public class MyType { + /// + /// Original triple slash comments. They should be replaced. + /// public MyType() { - } + } /* Trailing comments should remain untouched */ + // Original double slash comments. They should not be replaced (internal). internal MyType(int myProperty) { _myProperty = myProperty; - } + } // Trailing comments should remain untouched + + /// + /// Triple slash comments above private members should remain untouched. + /// + private int _otherProperty; + // Double slash comments above private members should remain untouched. private int _myProperty; + /// + /// Original triple slash comments. They should be replaced. + /// + // Original double slash comments. They should be replaced. public int MyProperty { - get { return _myProperty; } - set { _myProperty = value; } + get { return _myProperty; /* Internal comments should remain untouched. */ } + set { _myProperty = value; } // Internal comments should remain untouched } public int MyField = 1; public int MyIntMethod(int param1, int param2) { + // Internal comments should remain untouched. return MyField + param1 + param2; } @@ -32,12 +47,29 @@ public void MyVoidMethod() { } - public void MyTypeParamMethod() + /// + /// This method simulates a newly added API that did not have documentation in the docs xml. + /// The developer added the documentation in triple slash comments, so they should be preserved + /// and considered the source of truth. + /// + /// + /// These remarks are the source of truth. + /// + public void UndocumentedMethod() { } - public delegate void MyDelegate(object sender, object e); + public void MyTypeParamMethod(int param1) + { + } + + public delegate void MyDelegate(object sender, T e); public event MyDelegate MyEvent; + + public static MyType operator +(MyType value1, MyType value2) + { + return value1; + } } } From 83d02a650d153f48f781cc07d7730df5fcb9fc85 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 15 Jan 2021 12:16:29 -0800 Subject: [PATCH 53/65] Ignore extra info in xrefs when converting to crefs. Convert langwords. --- .../TripleSlashSyntaxRewriter.cs | 50 +++++++++---------- .../TestData/Basic/MyType.xml | 2 + .../TestData/Basic/SourceExpected.cs | 3 +- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index 86d59c2..26cacbc 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -88,6 +88,7 @@ public ... */ internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter { + private static readonly string[] ReservedKeywords = new[] { "abstract", "async", "await", "false", "null", "sealed", "static", "true", "virtual" }; private DocsCommentsContainer DocsComments { get; } private SemanticModel Model { get; } @@ -377,8 +378,8 @@ private static SyntaxTriviaList GetRemarks(DocsAPI api, SyntaxTriviaList leading { if (!api.Remarks.IsDocsEmpty()) { - string text = GetRemarksWithXmlParameters(api); - XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace, markdown: true); + string text = GetRemarksWithXmlElements(api); + XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace); XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(contents); return GetXmlTrivia(xmlRemarks, leadingWhitespace); } @@ -386,20 +387,33 @@ private static SyntaxTriviaList GetRemarks(DocsAPI api, SyntaxTriviaList leading return new(); } - private static string GetRemarksWithXmlParameters(IDocsAPI api) + /// + /// + /// + /// + private static string GetRemarksWithXmlElements(IDocsAPI api) { string remarks = api.Remarks; - if (!api.Remarks.IsDocsEmpty() && ( - api.Params.Any() || api.TypeParams.Any())) + if (!api.Remarks.IsDocsEmpty()) { - MatchCollection collection = Regex.Matches(api.Remarks, @"(?`(?[a-zA-Z0-9_]+)`)"); + remarks = Regex.Replace(remarks, @"", ""); + remarks = Regex.Replace(remarks, @"##[ ]?Remarks(\r?\n)*[\t ]*", ""); + remarks = Regex.Replace(remarks, @"(?[a-zA-Z0-9_\.]+)(?\?[a-zA-Z0-9_]+=[a-zA-Z0-9_])?>)", ""); + + MatchCollection collection = Regex.Matches(api.Remarks, @"(?`(?[a-zA-Z0-9_]+)`)"); foreach (Match match in collection) { string backtickedParam = match.Groups["backtickedParam"].Value; string paramName = match.Groups["paramName"].Value; - if (api.Params.Any(x => x.Name == paramName)) + if(ReservedKeywords.Any(x => x == paramName)) + { + remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); + } + else if (api.Params.Any(x => x.Name == paramName)) { remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); } @@ -409,7 +423,6 @@ private static string GetRemarksWithXmlParameters(IDocsAPI api) } } } - return remarks; } @@ -581,32 +594,15 @@ private static SyntaxTriviaList GetRelateds(List docsRelateds, Synt return relateds; } - private static string ReplaceText(string text, bool markdown) - { - if (markdown) - { - text = Regex.Replace(text, @"", ""); - text = Regex.Replace(text, @"##[ ]?Remarks(\r?\n)*[\t ]*", ""); - text = Regex.Replace(text, @"(?[a-zA-Z0-9_\.]+)>)", ""); - } - else - { - text = text.WithoutDocIdPrefixes(); - } - - return text; - } - /* XmlText XmlTextLiteralNewLineToken (XmlTextSyntax) -> endline XmlTextLiteralToken (XmlTextLiteralToken) -> [ text] Lead: DocumentationCommentExteriorTrivia (SyntaxTrivia) -> [ /// ] */ - private static XmlTextSyntax GetTextAsCommentedTokens(string text, SyntaxTriviaList leadingWhitespace, bool markdown = false) + private static XmlTextSyntax GetTextAsCommentedTokens(string text, SyntaxTriviaList leadingWhitespace) { - text = ReplaceText(text, markdown); + text = text.WithoutDocIdPrefixes(); // collapse newlines to a single one string whitespace = Regex.Replace(leadingWhitespace.ToFullString(), @"(\r?\n)+", ""); diff --git a/Tests/PortToTripleSlash/TestData/Basic/MyType.xml b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml index 61abfeb..54282e2 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/MyType.xml +++ b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml @@ -95,6 +95,8 @@ Multiple lines. Mentions the `param1`, the and the `param2`. +There are also a `true` and a `null`. + ]]> This is the ArgumentNullException thrown by MyIntMethod. It mentions the . diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index b724ff5..8cb3d0c 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -47,7 +47,8 @@ public int MyProperty /// This is the MyIntMethod return value. It mentions the . /// These are the MyIntMethod remarks. /// Multiple lines. - /// Mentions the , the and the . + /// Mentions the , the and the . + /// There are also a and a . /// This is the ArgumentNullException thrown by MyIntMethod. It mentions the . /// This is the IndexOutOfRangeException thrown by MyIntMethod. public int MyIntMethod(int param1, int param2) From 05c749f682d394546af2c4fd38fd49078478478b Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 15 Jan 2021 12:20:44 -0800 Subject: [PATCH 54/65] Added missing test for displayProperty which uncovered an unhandled bug when detecting that string. --- Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs | 2 +- Tests/PortToTripleSlash/TestData/Basic/MyType.xml | 2 +- Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index 26cacbc..96df932 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -401,7 +401,7 @@ private static string GetRemarksWithXmlElements(IDocsAPI api) remarks = Regex.Replace(remarks, @"", ""); remarks = Regex.Replace(remarks, @"##[ ]?Remarks(\r?\n)*[\t ]*", ""); - remarks = Regex.Replace(remarks, @"(?[a-zA-Z0-9_\.]+)(?\?[a-zA-Z0-9_]+=[a-zA-Z0-9_])?>)", ""); + remarks = Regex.Replace(remarks, @"(?[a-zA-Z0-9_\.]+)(?\?[a-zA-Z0-9_]+=[a-zA-Z0-9_]+)?>)", ""); MatchCollection collection = Regex.Matches(api.Remarks, @"(?`(?[a-zA-Z0-9_]+)`)"); diff --git a/Tests/PortToTripleSlash/TestData/Basic/MyType.xml b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml index 54282e2..77a6832 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/MyType.xml +++ b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml @@ -45,7 +45,7 @@ Multiple lines. These are the MyProperty remarks. -Multiple lines and a reference to the field . +Multiple lines and a reference to the field and the xref uses displayProperty, which should be ignored when porting. ]]> diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index 8cb3d0c..fbcb2e9 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -29,7 +29,7 @@ internal MyType(int myProperty) /// This is the MyProperty summary. /// This is the MyProperty value. /// These are the MyProperty remarks. - /// Multiple lines and a reference to the field . + /// Multiple lines and a reference to the field and the xref uses displayProperty, which should be ignored when porting. public int MyProperty { get { return _myProperty; /* Internal comments should remain untouched. */ } From ea1bb2b7215cbaaabbc380c2551a05391ed37358 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 28 Jan 2021 11:36:04 -0800 Subject: [PATCH 55/65] Advanced DocId detection in remarks, excluding prefixes that can't be converted to see crefs. Detect primitive types in see crefs and convert them to their simplified representation. Add unit tests to verify this. --- Libraries/Extensions.cs | 13 -- .../TripleSlashSyntaxRewriter.cs | 173 +++++++++++------- .../TestData/Basic/MyType.xml | 13 +- .../TestData/Basic/SourceExpected.cs | 8 +- 4 files changed, 125 insertions(+), 82 deletions(-) diff --git a/Libraries/Extensions.cs b/Libraries/Extensions.cs index 40a5d63..03966e4 100644 --- a/Libraries/Extensions.cs +++ b/Libraries/Extensions.cs @@ -38,19 +38,6 @@ public static string RemoveSubstrings(this string oldString, params string[] str // Checks if the passed string is considered "empty" according to the Docs repo rules. public static bool IsDocsEmpty(this string? s) => string.IsNullOrWhiteSpace(s) || s == Configuration.ToBeAdded; - - public static string WithoutDocIdPrefixes(this string text) - { - if (text.Length > 2 && text[1] == ':') - { - return text[2..]; - } - - return Regex.Replace( - input: text, - pattern: @"cref=""[a-zA-Z]{1}\:(?[a-zA-Z0-9\._]+)""", - replacement: "cref=\"${cref}\""); - } } } diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index 96df932..2167a0c 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -89,6 +89,27 @@ public ... internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter { private static readonly string[] ReservedKeywords = new[] { "abstract", "async", "await", "false", "null", "sealed", "static", "true", "virtual" }; + + 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" } + }; + private DocsCommentsContainer DocsComments { get; } private SemanticModel Model { get; } @@ -213,6 +234,8 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod #endregion + #region Visit helpers + private SyntaxNode? VisitType(SyntaxNode? node, ISymbol? symbol) { if (node == null || symbol == null) @@ -317,6 +340,38 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod return node; } + private bool TryGetMember(SyntaxNode node, [NotNullWhen(returnValue: true)] out DocsMember? member) + { + member = null; + if (Model.GetDeclaredSymbol(node) is ISymbol symbol) + { + string? docId = symbol.GetDocumentationCommentId(); + if (!string.IsNullOrWhiteSpace(docId)) + { + member = DocsComments.Members.FirstOrDefault(m => m.DocId == docId); + } + } + + return member != null; + } + + private bool TryGetType(ISymbol symbol, [NotNullWhen(returnValue: true)] out DocsType? type) + { + type = null; + + string? docId = symbol.GetDocumentationCommentId(); + if (!string.IsNullOrWhiteSpace(docId)) + { + type = DocsComments.Types.FirstOrDefault(t => t.DocId == docId); + } + + return type != null; + } + + #endregion + + #region Syntax manipulation + private static SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxNode node, params SyntaxTriviaList[] trivias) { SyntaxTriviaList finalTrivia = new(); @@ -387,45 +442,6 @@ private static SyntaxTriviaList GetRemarks(DocsAPI api, SyntaxTriviaList leading return new(); } - /// - /// - /// - /// - private static string GetRemarksWithXmlElements(IDocsAPI api) - { - string remarks = api.Remarks; - - if (!api.Remarks.IsDocsEmpty()) - { - remarks = Regex.Replace(remarks, @"", ""); - remarks = Regex.Replace(remarks, @"##[ ]?Remarks(\r?\n)*[\t ]*", ""); - remarks = Regex.Replace(remarks, @"(?[a-zA-Z0-9_\.]+)(?\?[a-zA-Z0-9_]+=[a-zA-Z0-9_]+)?>)", ""); - - MatchCollection collection = Regex.Matches(api.Remarks, @"(?`(?[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)) - { - remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); - } - else if (api.Params.Any(x => x.Name == paramName)) - { - remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); - } - else if (api.TypeParams.Any(x => x.Name == paramName)) - { - remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); - } - } - } - return remarks; - } - private static SyntaxTriviaList GetValue(DocsMember api, SyntaxTriviaList leadingWhitespace) { if (!api.Value.IsDocsEmpty()) @@ -503,8 +519,7 @@ private static SyntaxTriviaList GetException(string cref, string text, SyntaxTri { if (!text.IsDocsEmpty()) { - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutDocIdPrefixes())); - //XmlTextSyntax contents = SyntaxFactory.XmlText(GetTextAsTokens(text.WithoutPrefix(), leadingWhitespace)); + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(RemoveDocIdPrefixes(cref))); XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace); XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents); return GetXmlTrivia(element, leadingWhitespace); @@ -529,7 +544,8 @@ private static SyntaxTriviaList GetExceptions(List docsExceptions private static SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace) { - TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref.WithoutDocIdPrefixes())); + cref = ReplacePrimitiveTypes(cref); + TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref)); XmlEmptyElementSyntax element = SyntaxFactory.XmlSeeAlsoElement(crefSyntax); return GetXmlTrivia(element, leadingWhitespace); } @@ -550,7 +566,8 @@ private static SyntaxTriviaList GetSeeAlsos(List docsSeeAlsoCrefs, Synta private static SyntaxTriviaList GetAltMember(string cref, SyntaxTriviaList leadingWhitespace) { - XmlAttributeSyntax attribute = SyntaxFactory.XmlTextAttribute("cref", cref.WithoutDocIdPrefixes()); + cref = ReplacePrimitiveTypes(cref); + XmlAttributeSyntax attribute = SyntaxFactory.XmlTextAttribute("cref", cref); XmlEmptyElementSyntax emptyElement = SyntaxFactory.XmlEmptyElement(SyntaxFactory.XmlName(SyntaxFactory.Identifier("altmember")), new SyntaxList(attribute)); return GetXmlTrivia(emptyElement, leadingWhitespace); } @@ -594,15 +611,9 @@ private static SyntaxTriviaList GetRelateds(List docsRelateds, Synt return relateds; } - /* - XmlText - XmlTextLiteralNewLineToken (XmlTextSyntax) -> endline - XmlTextLiteralToken (XmlTextLiteralToken) -> [ text] - Lead: DocumentationCommentExteriorTrivia (SyntaxTrivia) -> [ /// ] - */ private static XmlTextSyntax GetTextAsCommentedTokens(string text, SyntaxTriviaList leadingWhitespace) { - text = text.WithoutDocIdPrefixes(); + text = ReplacePrimitiveTypes(text); // collapse newlines to a single one string whitespace = Regex.Replace(leadingWhitespace.ToFullString(), @"(\r?\n)+", ""); @@ -662,32 +673,64 @@ private static SyntaxTriviaList GetXmlTrivia(string name, SyntaxList", ""); + remarks = Regex.Replace(remarks, @"##[ ]?Remarks(\r?\n)*[\t ]*", ""); + remarks = Regex.Replace(remarks, @"(?[a-zA-Z0-9_,\.\[\]\(\)`\{\}\@\+\*\&\^\#]+)(?%2A)?(?\?[a-zA-Z0-9_]+=[a-zA-Z0-9_]+)?>)", ""); + + MatchCollection collection = Regex.Matches(api.Remarks, @"(?`(?[a-zA-Z0-9_]+)`)"); + + foreach (Match match in collection) { - member = DocsComments.Members.FirstOrDefault(m => m.DocId == docId); + string backtickedParam = match.Groups["backtickedParam"].Value; + string paramName = match.Groups["paramName"].Value; + if (ReservedKeywords.Any(x => x == paramName)) + { + remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); + } + else if (api.Params.Any(x => x.Name == paramName)) + { + remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); + } + else if (api.TypeParams.Any(x => x.Name == paramName)) + { + remarks = Regex.Replace(remarks, $"{backtickedParam}", $""); + } } - } - return member != null; + remarks = ReplacePrimitiveTypes(remarks); + } + return remarks; } - private bool TryGetType(ISymbol symbol, [NotNullWhen(returnValue: true)] out DocsType? type) + private static string RemoveDocIdPrefixes(string text) { - type = null; - - string? docId = symbol.GetDocumentationCommentId(); - if (!string.IsNullOrWhiteSpace(docId)) + if (text.Length > 2 && text[1] == ':') { - type = DocsComments.Types.FirstOrDefault(t => t.DocId == docId); + return text[2..]; } - return type != null; + text = Regex.Replace(text, @"cref=""[a-zA-Z]{1}\:", "cref=\""); + + return text; } + + private static string ReplacePrimitiveTypes(string text) + { + text = RemoveDocIdPrefixes(text); + foreach ((string key, string value) in PrimitiveTypes) + { + text = Regex.Replace(text, @$" 1 - This is the MyField summary. + This is the MyField summary. + +There is a primitive type here. here. + Multiple lines. ]]> @@ -112,7 +116,8 @@ There are also a `true` and a `null`. This is the MyVoidMethod summary. - + . +Also mentions an overloaded method DocID: . + +And also mentions an overloaded method DocID with displayProperty which should be ignored when porting: . + ]]> This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs index fbcb2e9..cabbcd2 100644 --- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs +++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs @@ -36,8 +36,10 @@ public int MyProperty set { _myProperty = value; } // Internal comments should remain untouched } - /// This is the MyField summary. + /// This is the MyField summary. + /// There is a primitive type here. /// These are the MyField remarks. + /// There is a primitive type here. /// Multiple lines. public int MyField = 1; @@ -60,7 +62,9 @@ public int MyIntMethod(int param1, int param2) /// This is the MyVoidMethod summary. /// These are the MyVoidMethod remarks. /// Multiple lines. - /// Mentions the . + /// Mentions the . + /// Also mentions an overloaded method DocID: . + /// And also mentions an overloaded method DocID with displayProperty which should be ignored when porting: . /// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the . /// This is the IndexOutOfRangeException thrown by MyVoidMethod. /// -or- From d2e5f9889f58152c619a118e0929d1be661c7398 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 28 Jan 2021 11:55:02 -0800 Subject: [PATCH 56/65] Add more asserts messages to check files and dirs exist in CI test execution. --- Tests/PortToDocs/PortToDocsTestData.cs | 28 +++++++++++++++++--------- Tests/TestDirectory.cs | 2 +- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/Tests/PortToDocs/PortToDocsTestData.cs b/Tests/PortToDocs/PortToDocsTestData.cs index 38a6d5d..8b7bb09 100644 --- a/Tests/PortToDocs/PortToDocsTestData.cs +++ b/Tests/PortToDocs/PortToDocsTestData.cs @@ -28,10 +28,16 @@ internal PortToDocsTestData( namespaceName = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName; IntelliSenseAndDLLDir = tempDir.CreateSubdirectory(IntellisenseAndDllDirName); + Assert.True(IntelliSenseAndDLLDir.Exists, "Verify IntelliSense and DLL directory exists."); + DirectoryInfo tripleSlashAssemblyDir = IntelliSenseAndDLLDir.CreateSubdirectory(assemblyName); + Assert.True(tripleSlashAssemblyDir.Exists, "Verify triple slash and assembly directory exists."); DocsDir = tempDir.CreateSubdirectory(DocsDirName); + Assert.True(DocsDir.Exists, "Verify docs directory exists."); + DirectoryInfo docsAssemblyDir = DocsDir.CreateSubdirectory(namespaceName); + Assert.True(docsAssemblyDir.Exists, "Verify docs assembly directory exists."); string testDataPath = Path.Combine(TestDataRootDirPath, testDataDir); @@ -39,32 +45,36 @@ internal PortToDocsTestData( string docsOriginalFilePath = Path.Combine(testDataPath, "DocsOriginal.xml"); string docsExpectedFilePath = Path.Combine(testDataPath, "DocsExpected.xml"); - Assert.True(File.Exists(tripleSlashOriginalFilePath)); - Assert.True(File.Exists(docsOriginalFilePath)); - Assert.True(File.Exists(docsExpectedFilePath)); + Assert.True(File.Exists(tripleSlashOriginalFilePath), "Verify triple slash original file exists."); + Assert.True(File.Exists(docsOriginalFilePath), "Verify docs original file exists."); + Assert.True(File.Exists(docsExpectedFilePath), "Verify docs expected file exists."); DocsOriginFilePath = Path.Combine(tripleSlashAssemblyDir.FullName, $"{typeName}.xml"); ActualFilePath = Path.Combine(docsAssemblyDir.FullName, $"{typeName}.xml"); ExpectedFilePath = Path.Combine(tempDir.FullPath, "DocsExpected.xml"); File.Copy(tripleSlashOriginalFilePath, DocsOriginFilePath); + Assert.True(File.Exists(DocsOriginFilePath), "Verify triple slash original file (copied) exists."); + File.Copy(docsOriginalFilePath, ActualFilePath); - File.Copy(docsExpectedFilePath, ExpectedFilePath); + Assert.True(File.Exists(ActualFilePath), "Verify docs original file (copied) exists."); - Assert.True(File.Exists(DocsOriginFilePath)); - Assert.True(File.Exists(ActualFilePath)); - Assert.True(File.Exists(ExpectedFilePath)); + File.Copy(docsExpectedFilePath, ExpectedFilePath); + Assert.True(File.Exists(ExpectedFilePath), "Verify docs expected file (copied) exists."); if (!skipInterfaceImplementations) { string interfaceFilePath = Path.Combine(testDataPath, "DocsInterface.xml"); - Assert.True(File.Exists(interfaceFilePath)); + Assert.True(File.Exists(interfaceFilePath), "Verify docs interface file exists."); string interfaceAssembly = "System"; + DirectoryInfo interfaceAssemblyDir = DocsDir.CreateSubdirectory(interfaceAssembly); + Assert.True(interfaceAssemblyDir.Exists, "Verify interface assembly directory exists."); + InterfaceFilePath = Path.Combine(interfaceAssemblyDir.FullName, "IMyInterface.xml"); File.Copy(interfaceFilePath, InterfaceFilePath); - Assert.True(File.Exists(InterfaceFilePath)); + Assert.True(File.Exists(InterfaceFilePath), "Verify docs interface file (copied) exists."); } } } diff --git a/Tests/TestDirectory.cs b/Tests/TestDirectory.cs index 46e0a2b..0e47f70 100644 --- a/Tests/TestDirectory.cs +++ b/Tests/TestDirectory.cs @@ -15,7 +15,7 @@ public TestDirectory() string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); DirInfo = new DirectoryInfo(path); DirInfo.Create(); - Assert.True(DirInfo.Exists); + Assert.True(DirInfo.Exists, "Verify root test directory exists."); } public DirectoryInfo CreateSubdirectory(string dirName) From fba6668ac381dd30bb333eab76ce39bc6490adff Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 28 Jan 2021 12:17:32 -0800 Subject: [PATCH 57/65] Print more details when error in line comparison in ToDocsTests --- Tests/PortToDocs/PortToDocsTests.cs | 30 +++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/Tests/PortToDocs/PortToDocsTests.cs b/Tests/PortToDocs/PortToDocsTests.cs index 3343128..f3d18e0 100644 --- a/Tests/PortToDocs/PortToDocsTests.cs +++ b/Tests/PortToDocs/PortToDocsTests.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using Xunit; @@ -145,14 +146,39 @@ private void Verify(PortToDocsTestData testData) string[] expectedLines = File.ReadAllLines(testData.ExpectedFilePath); string[] actualLines = File.ReadAllLines(testData.ActualFilePath); + Assert.Equal(expectedLines.Length, actualLines.Length); + for (int i = 0; i < expectedLines.Length; i++) { string expectedLine = expectedLines[i]; string actualLine = actualLines[i]; + + // Print some more details before asserting + if (expectedLine != actualLine) + { + if ((i - 2) >= 0) + { + Console.WriteLine("[-2] " + expectedLines[i - 2]); + } + if ((i - 1) >= 0) + { + Console.WriteLine("[-1] " + expectedLines[i - 1]); + } + + Console.WriteLine("[:(] " + expectedLine); + + if ((i + 1) < expectedLines.Length) + { + Console.WriteLine("[+1] " + expectedLines[i + 1]); + } + if ((i + 2) < expectedLines.Length) + { + Console.WriteLine("[+2] " + expectedLines[i + 2]); + } + } + Assert.Equal(expectedLine, actualLine); } - - Assert.Equal(expectedLines.Length, actualLines.Length); } } From 36448aab9bee358ea4bb0fa4ecb45bdf15b0f61b Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 28 Jan 2021 12:51:45 -0800 Subject: [PATCH 58/65] Add BasePortTests with output helper --- Tests/BasePortTests.cs | 11 +++++++++++ Tests/PortToDocs/PortToDocsTests.cs | 18 +++++++++++------- .../PortToTripleSlashTests.cs | 13 ++++++------- 3 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 Tests/BasePortTests.cs diff --git a/Tests/BasePortTests.cs b/Tests/BasePortTests.cs new file mode 100644 index 0000000..377f5f7 --- /dev/null +++ b/Tests/BasePortTests.cs @@ -0,0 +1,11 @@ +using Xunit.Abstractions; + +namespace Libraries.Tests +{ + public abstract class BasePortTests + { + protected ITestOutputHelper Output { get; private set; } + + public BasePortTests(ITestOutputHelper output) => Output = output; + } +} diff --git a/Tests/PortToDocs/PortToDocsTests.cs b/Tests/PortToDocs/PortToDocsTests.cs index f3d18e0..44a6f05 100644 --- a/Tests/PortToDocs/PortToDocsTests.cs +++ b/Tests/PortToDocs/PortToDocsTests.cs @@ -1,11 +1,15 @@ -using System; using System.IO; using Xunit; +using Xunit.Abstractions; namespace Libraries.Tests { - public class PortToDocsTests + public class PortToDocsTests : BasePortTests { + public PortToDocsTests(ITestOutputHelper output) : base(output) + { + } + [Fact] // Verifies the basic case of porting all regular fields. public void Port_Basic() @@ -158,22 +162,22 @@ private void Verify(PortToDocsTestData testData) { if ((i - 2) >= 0) { - Console.WriteLine("[-2] " + expectedLines[i - 2]); + Output.WriteLine("[-2] " + expectedLines[i - 2]); } if ((i - 1) >= 0) { - Console.WriteLine("[-1] " + expectedLines[i - 1]); + Output.WriteLine("[-1] " + expectedLines[i - 1]); } - Console.WriteLine("[:(] " + expectedLine); + Output.WriteLine("[:(] " + expectedLine); if ((i + 1) < expectedLines.Length) { - Console.WriteLine("[+1] " + expectedLines[i + 1]); + Output.WriteLine("[+1] " + expectedLines[i + 1]); } if ((i + 2) < expectedLines.Length) { - Console.WriteLine("[+2] " + expectedLines[i + 2]); + Output.WriteLine("[+2] " + expectedLines[i + 2]); } } diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs index 4170247..7c62cbc 100644 --- a/Tests/PortToTripleSlash/PortToTripleSlashTests.cs +++ b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs @@ -1,17 +1,16 @@ #nullable enable -using Microsoft.Build.Locator; -using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.Loader; using Xunit; +using Xunit.Abstractions; namespace Libraries.Tests { - public class PortToTripleSlashTests + public class PortToTripleSlashTests : BasePortTests { + public PortToTripleSlashTests(ITestOutputHelper output) : base(output) + { + } + [Fact] public void Port_Basic() { From 93d13fd0e7570ead83c9091af611efe5462e3fbf Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 28 Jan 2021 13:16:22 -0800 Subject: [PATCH 59/65] Move back line compare to the end --- Tests/PortToDocs/PortToDocsTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/PortToDocs/PortToDocsTests.cs b/Tests/PortToDocs/PortToDocsTests.cs index 44a6f05..af131f2 100644 --- a/Tests/PortToDocs/PortToDocsTests.cs +++ b/Tests/PortToDocs/PortToDocsTests.cs @@ -150,8 +150,6 @@ private void Verify(PortToDocsTestData testData) string[] expectedLines = File.ReadAllLines(testData.ExpectedFilePath); string[] actualLines = File.ReadAllLines(testData.ActualFilePath); - Assert.Equal(expectedLines.Length, actualLines.Length); - for (int i = 0; i < expectedLines.Length; i++) { string expectedLine = expectedLines[i]; @@ -183,6 +181,9 @@ private void Verify(PortToDocsTestData testData) Assert.Equal(expectedLine, actualLine); } + + // Check at the end, because we first want to fail on different lines + Assert.Equal(expectedLines.Length, actualLines.Length); } } From 0d6b9337d5d014649f951b32c496963a788e9e44 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 28 Jan 2021 13:33:13 -0800 Subject: [PATCH 60/65] Print expected and actual extra lines before asserting --- Tests/PortToDocs/PortToDocsTests.cs | 54 +++++++++++++++++++---------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/Tests/PortToDocs/PortToDocsTests.cs b/Tests/PortToDocs/PortToDocsTests.cs index af131f2..59c94be 100644 --- a/Tests/PortToDocs/PortToDocsTests.cs +++ b/Tests/PortToDocs/PortToDocsTests.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using Xunit; using Xunit.Abstractions; @@ -152,31 +153,20 @@ private void Verify(PortToDocsTestData testData) for (int i = 0; i < expectedLines.Length; i++) { + Assert.True(i < expectedLines.Length); + Assert.True(i < actualLines.Length); + string expectedLine = expectedLines[i]; string actualLine = actualLines[i]; // Print some more details before asserting if (expectedLine != actualLine) { - if ((i - 2) >= 0) - { - Output.WriteLine("[-2] " + expectedLines[i - 2]); - } - if ((i - 1) >= 0) - { - Output.WriteLine("[-1] " + expectedLines[i - 1]); - } - - Output.WriteLine("[:(] " + expectedLine); - - if ((i + 1) < expectedLines.Length) - { - Output.WriteLine("[+1] " + expectedLines[i + 1]); - } - if ((i + 2) < expectedLines.Length) - { - Output.WriteLine("[+2] " + expectedLines[i + 2]); - } + string expected = GetProblematicLines("Expected", expectedLines, i); + string actual = GetProblematicLines("Actual", actualLines, i); + + Output.WriteLine(expected); + Output.WriteLine(actual); } Assert.Equal(expectedLine, actualLine); @@ -186,5 +176,31 @@ private void Verify(PortToDocsTestData testData) Assert.Equal(expectedLines.Length, actualLines.Length); } + + private static string GetProblematicLines(string title, string[] lines, int i) + { + string output = $"{title}:{Environment.NewLine}"; + if ((i - 2) >= 0) + { + output += $"[{i - 2}] {lines[i - 2]}{Environment.NewLine}"; + } + if ((i - 1) >= 0) + { + output += $"[{i - 1}] {lines[i - 1]}{Environment.NewLine}"; + } + + output += $"[{i}] {lines[i]}{Environment.NewLine}"; + + if ((i + 1) < lines.Length) + { + output += $"[{i + 1}] {lines[i + 1]}{Environment.NewLine}"; + } + if ((i + 2) < lines.Length) + { + output += $"[{i + 2}] {lines[i + 2]}{Environment.NewLine}"; + } + + return output; + } } } From ff648604be6f8df55375ba2f21e87674fbc19b9c Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 28 Jan 2021 13:42:28 -0800 Subject: [PATCH 61/65] More lines --- Tests/PortToDocs/PortToDocsTests.cs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/Tests/PortToDocs/PortToDocsTests.cs b/Tests/PortToDocs/PortToDocsTests.cs index 59c94be..958ec15 100644 --- a/Tests/PortToDocs/PortToDocsTests.cs +++ b/Tests/PortToDocs/PortToDocsTests.cs @@ -177,27 +177,26 @@ private void Verify(PortToDocsTestData testData) } - private static string GetProblematicLines(string title, string[] lines, int i) + private static string GetProblematicLines(string title, string[] lines, int lineNumber) { string output = $"{title}:{Environment.NewLine}"; - if ((i - 2) >= 0) - { - output += $"[{i - 2}] {lines[i - 2]}{Environment.NewLine}"; - } - if ((i - 1) >= 0) + + for (int i = 0; i <= 4; i++) { - output += $"[{i - 1}] {lines[i - 1]}{Environment.NewLine}"; + if ((lineNumber - i) >= 0) + { + output += $"[{lineNumber - i}] {lines[lineNumber - i]}{Environment.NewLine}"; + } } - output += $"[{i}] {lines[i]}{Environment.NewLine}"; + output += $"[{lineNumber}] {lines[lineNumber]}{Environment.NewLine}"; - if ((i + 1) < lines.Length) + for (int i = 0; i <= 4; i++) { - output += $"[{i + 1}] {lines[i + 1]}{Environment.NewLine}"; - } - if ((i + 2) < lines.Length) - { - output += $"[{i + 2}] {lines[i + 2]}{Environment.NewLine}"; + if ((lineNumber + i) < lines.Length) + { + output += $"[{lineNumber + i}] {lines[lineNumber + i]}{Environment.NewLine}"; + } } return output; From cdc8fb1103ef42e200c212ef4de1f4b87b90948d Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 28 Jan 2021 13:46:16 -0800 Subject: [PATCH 62/65] bug --- Tests/PortToDocs/PortToDocsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/PortToDocs/PortToDocsTests.cs b/Tests/PortToDocs/PortToDocsTests.cs index 958ec15..9b47f41 100644 --- a/Tests/PortToDocs/PortToDocsTests.cs +++ b/Tests/PortToDocs/PortToDocsTests.cs @@ -181,7 +181,7 @@ private static string GetProblematicLines(string title, string[] lines, int line { string output = $"{title}:{Environment.NewLine}"; - for (int i = 0; i <= 4; i++) + for (int i = 5; i >= 1; i--) { if ((lineNumber - i) >= 0) { @@ -191,7 +191,7 @@ private static string GetProblematicLines(string title, string[] lines, int line output += $"[{lineNumber}] {lines[lineNumber]}{Environment.NewLine}"; - for (int i = 0; i <= 4; i++) + for (int i = 1; i <= 5; i++) { if ((lineNumber + i) < lines.Length) { From e7e3c0386a7e3dad1735bb9a8e65f5f2fb6fc999 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 28 Jan 2021 13:51:16 -0800 Subject: [PATCH 63/65] Append interface remark lines one by one --- Libraries/ToDocsPorter.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Libraries/ToDocsPorter.cs b/Libraries/ToDocsPorter.cs index 67828a3..8e40963 100644 --- a/Libraries/ToDocsPorter.cs +++ b/Libraries/ToDocsPorter.cs @@ -240,7 +240,11 @@ private void TryPortMissingRemarksForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlM string cleanedInterfaceRemarks = string.Empty; if (!interfacedMember.Remarks.Contains(Configuration.ToBeAdded)) { - cleanedInterfaceRemarks = interfacedMember.Remarks.RemoveSubstrings("##Remarks", "## Remarks", ""); + string interfaceMemberRemarks = interfacedMember.Remarks.RemoveSubstrings("##Remarks", "## Remarks", ""); + foreach (string line in interfaceMemberRemarks.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + cleanedInterfaceRemarks += line + Environment.NewLine; + } } // Only port the interface remarks if the user desired that From 220216f801b963922af00a9f1beed7d06a36b769 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 28 Jan 2021 14:09:36 -0800 Subject: [PATCH 64/65] Newlines in docs --- Libraries/ToDocsPorter.cs | 6 ++++-- Libraries/XmlHelper.cs | 3 +-- .../Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Libraries/ToDocsPorter.cs b/Libraries/ToDocsPorter.cs index 8e40963..d74f994 100644 --- a/Libraries/ToDocsPorter.cs +++ b/Libraries/ToDocsPorter.cs @@ -235,15 +235,17 @@ private void TryPortMissingRemarksForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlM string interfacedMemberTypeDocIdNoPrefix = interfacedMember.ParentType.DocId[2..]; // Special text for EIIs in Remarks - string eiiMessage = $"This member is an explicit interface member implementation. It can be used only when the instance is cast to an interface.{Environment.NewLine + Environment.NewLine}"; + string eiiMessage = $"This member is an explicit interface member implementation. It can be used only when the instance is cast to an interface."; string cleanedInterfaceRemarks = string.Empty; if (!interfacedMember.Remarks.Contains(Configuration.ToBeAdded)) { + cleanedInterfaceRemarks += Environment.NewLine; + string interfaceMemberRemarks = interfacedMember.Remarks.RemoveSubstrings("##Remarks", "## Remarks", ""); foreach (string line in interfaceMemberRemarks.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { - cleanedInterfaceRemarks += line + Environment.NewLine; + cleanedInterfaceRemarks += Environment.NewLine + line; } } diff --git a/Libraries/XmlHelper.cs b/Libraries/XmlHelper.cs index 8f33d27..7e0b1d6 100644 --- a/Libraries/XmlHelper.cs +++ b/Libraries/XmlHelper.cs @@ -145,8 +145,7 @@ public static void SaveFormattedAsMarkdown(XElement element, string newValue, bo XElement xeFormat = new XElement("format"); - string updatedValue = RemoveUndesiredEndlines(newValue); - updatedValue = SubstituteRemarksRegexPatterns(updatedValue); + string updatedValue = SubstituteRemarksRegexPatterns(newValue); updatedValue = ReplaceMarkdownPatterns(updatedValue); string remarksTitle = string.Empty; diff --git a/Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml b/Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml index 33bdbee..c41cae6 100644 --- a/Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml +++ b/Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml @@ -92,7 +92,8 @@ These are the method remarks. They are pointing to a param: `myParam`. ## Remarks This member is an explicit interface member implementation. It can be used only when the instance is cast to an interface. - Original interface method remarks that should show up in interface implementations if -skipInterfaceRemarks is set to `false`. + +Original interface method remarks that should show up in interface implementations if -skipInterfaceRemarks is set to `false`. ]]> From 0bd418fa1f3e7563e151de8b5ca76662fb182eb9 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 28 Jan 2021 16:22:04 -0800 Subject: [PATCH 65/65] Simplify primitive type replacement. --- Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs index 2167a0c..4ea8b26 100644 --- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs +++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs @@ -726,7 +726,7 @@ private static string ReplacePrimitiveTypes(string text) text = RemoveDocIdPrefixes(text); foreach ((string key, string value) in PrimitiveTypes) { - text = Regex.Replace(text, @$"