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
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
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/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/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/Configuration.cs b/Libraries/Configuration.cs
similarity index 77%
rename from DocsPortingTool/Configuration.cs
rename to Libraries/Configuration.cs
index d48c7d5..f2843ac 100644
--- a/DocsPortingTool/Configuration.cs
+++ b/Libraries/Configuration.cs
@@ -1,16 +1,26 @@
-using System;
+#nullable enable
+using System;
using System.Collections.Generic;
using System.IO;
-namespace DocsPortingTool
+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,
@@ -20,6 +30,7 @@ private enum Mode
IncludedNamespaces,
IncludedTypes,
Initial,
+ IntelliSense,
PortExceptionsExisting,
PortExceptionsNew,
PortMemberParams,
@@ -35,26 +46,29 @@ private enum Mode
PrintUndoc,
Save,
SkipInterfaceImplementations,
- SkipInterfaceRemarks,
- TripleSlash
+ 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[] ForbiddenDirectories = new[] { "binplacePackages", "docs", "mscorlib", "native", "netfx", "netstandard", "pkg", "Product", "ref", "runtime", "shimsTargetRuntime", "testhost", "tests", "winrt" };
+ public static readonly string[] ForbiddenBinSubdirectories = new[] { "binplacePackages", "docs", "mscorlib", "native", "netfx", "netstandard", "pkg", "Product", "ref", "runtime", "shimsTargetRuntime", "testhost", "tests", "winrt" };
- public List DirsTripleSlashXmls { get; } = new List();
+ public readonly string BinLogPath = "output.binlog";
+ public bool BinLogger { get; private set; } = false;
+ 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 HashSet IncludedAssemblies { get; } = new HashSet();
+ public bool DisablePrompts { get; set; } = false;
+ public int ExceptionCollisionThreshold { get; set; } = 70;
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 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;
@@ -78,7 +92,7 @@ private enum Mode
public bool SkipInterfaceImplementations { get; set; } = false;
public bool SkipInterfaceRemarks { get; set; } = true;
- public static Configuration GetFromCommandLineArguments(string[] args)
+ public static Configuration GetCLIArgumentsForDocsPortingTool(string[] args)
{
Mode mode = Mode.Initial;
@@ -86,7 +100,7 @@ public static Configuration GetFromCommandLineArguments(string[] args)
if (args == null || args.Length == 0)
{
- Log.LogErrorPrintHelpAndExit("No arguments passed to the executable.");
+ Log.PrintHelpAndError("No arguments passed to the executable.");
}
Configuration config = new Configuration();
@@ -95,6 +109,36 @@ public static Configuration GetFromCommandLineArguments(string[] args)
{
switch (mode)
{
+ case Mode.BinLog:
+ {
+ config.BinLogger = ParseOrExit(arg, "Create a binlog");
+ mode = Mode.Initial;
+ break;
+ }
+
+ case Mode.CsProj:
+ {
+ if (string.IsNullOrWhiteSpace(arg))
+ {
+ throw new Exception("You must specify a *.csproj path.");
+ }
+ else if (!File.Exists(arg))
+ {
+ throw new Exception($"The *.csproj file does not exist: {arg}");
+ }
+ else
+ {
+ string ext = Path.GetExtension(arg).ToUpperInvariant();
+ if (ext != ".CSPROJ")
+ {
+ throw new Exception($"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");
@@ -102,6 +146,25 @@ public static Configuration GetFromCommandLineArguments(string[] args)
break;
}
+ case Mode.Direction:
+ {
+ switch (arg.ToUpperInvariant())
+ {
+ case "TODOCS":
+ config.Direction = PortingDirection.ToDocs;
+ break;
+ case "TOTRIPLESLASH":
+ config.Direction = PortingDirection.ToTripleSlash;
+ // Must always skip to avoid loading interface docs files to memory
+ config.SkipInterfaceImplementations = true;
+ break;
+ default:
+ throw new Exception($"Unrecognized direction value: {arg}");
+ }
+ mode = Mode.Initial;
+ break;
+ }
+
case Mode.Docs:
{
string[] splittedDirPaths = arg.Split(',', StringSplitOptions.RemoveEmptyEntries);
@@ -112,7 +175,7 @@ public static Configuration GetFromCommandLineArguments(string[] args)
DirectoryInfo dirInfo = new DirectoryInfo(dirPath);
if (!dirInfo.Exists)
{
- Log.LogErrorAndExit($"This Docs xml directory does not exist: {dirPath}");
+ throw new Exception($"This Docs xml directory does not exist: {dirPath}");
}
config.DirsDocsXml.Add(dirInfo);
@@ -121,18 +184,17 @@ public static Configuration GetFromCommandLineArguments(string[] args)
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}");
+ throw new Exception($"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}");
+ throw new Exception($"Value needs to be between 0 and 100: {value}");
}
config.ExceptionCollisionThreshold = value;
@@ -157,7 +219,7 @@ public static Configuration GetFromCommandLineArguments(string[] args)
}
else
{
- Log.LogErrorPrintHelpAndExit("You must specify at least one assembly.");
+ Log.PrintHelpAndError("You must specify at least one assembly.");
}
mode = Mode.Initial;
@@ -179,7 +241,7 @@ public static Configuration GetFromCommandLineArguments(string[] args)
}
else
{
- Log.LogErrorPrintHelpAndExit("You must specify at least one namespace.");
+ Log.PrintHelpAndError("You must specify at least one namespace.");
}
mode = Mode.Initial;
@@ -201,7 +263,7 @@ public static Configuration GetFromCommandLineArguments(string[] args)
}
else
{
- Log.LogErrorPrintHelpAndExit("You must specify at least one type name.");
+ Log.PrintHelpAndError("You must specify at least one type name.");
}
mode = Mode.Initial;
@@ -223,7 +285,7 @@ public static Configuration GetFromCommandLineArguments(string[] args)
}
else
{
- Log.LogErrorPrintHelpAndExit("You must specify at least one assembly.");
+ Log.PrintHelpAndError("You must specify at least one assembly.");
}
mode = Mode.Initial;
@@ -245,7 +307,7 @@ public static Configuration GetFromCommandLineArguments(string[] args)
}
else
{
- Log.LogErrorPrintHelpAndExit("You must specify at least one namespace.");
+ Log.PrintHelpAndError("You must specify at least one namespace.");
}
mode = Mode.Initial;
@@ -267,7 +329,7 @@ public static Configuration GetFromCommandLineArguments(string[] args)
}
else
{
- Log.LogErrorPrintHelpAndExit("You must specify at least one type name.");
+ Log.PrintHelpAndError("You must specify at least one type name.");
}
mode = Mode.Initial;
@@ -278,6 +340,18 @@ public static Configuration GetFromCommandLineArguments(string[] args)
{
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;
@@ -320,6 +394,10 @@ public static Configuration GetFromCommandLineArguments(string[] args)
mode = Mode.IncludedTypes;
break;
+ case "-INTELLISENSE":
+ mode = Mode.IntelliSense;
+ break;
+
case "-PORTEXCEPTIONSEXISTING":
mode = Mode.PortExceptionsExisting;
break;
@@ -384,16 +462,34 @@ public static Configuration GetFromCommandLineArguments(string[] args)
mode = Mode.SkipInterfaceRemarks;
break;
- case "-TRIPLESLASH":
- mode = Mode.TripleSlash;
- break;
default:
- Log.LogErrorPrintHelpAndExit($"Unrecognized argument: {arg}");
+ Log.PrintHelpAndError($"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)
+ {
+ throw new Exception($"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");
@@ -506,30 +602,9 @@ public static Configuration GetFromCommandLineArguments(string[] args)
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.");
+ Log.PrintHelpAndError("Unexpected mode.");
break;
}
}
@@ -537,22 +612,33 @@ public static Configuration GetFromCommandLineArguments(string[] args)
if (mode != Mode.Initial)
{
- Log.LogErrorPrintHelpAndExit("You missed an argument value.");
+ Log.PrintHelpAndError("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)}.");
+ 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.PrintHelpAndError($"You must specify at least one IntelliSense & DLL folder using '-{nameof(Mode.IntelliSense)}'.");
+ }
}
- if (config.DirsTripleSlashXmls.Count == 0)
+ if (config.Direction == PortingDirection.ToTripleSlash)
{
- Log.LogErrorPrintHelpAndExit($"You must specify at least one triple slash xml folder path with {nameof(TripleSlash)}.");
+ if (config.CsProj == null)
+ {
+ Log.PrintHelpAndError($"You must specify a *.csproj file using '-{nameof(Mode.CsProj)}'.");
+ }
}
if (config.IncludedAssemblies.Count == 0)
{
- Log.LogErrorPrintHelpAndExit($"You must specify at least one assembly with {nameof(IncludedAssemblies)}.");
+ Log.PrintHelpAndError($"You must specify at least one assembly with {nameof(IncludedAssemblies)}.");
}
return config;
@@ -563,7 +649,7 @@ private static bool ParseOrExit(string arg, string paramFriendlyName)
{
if (!bool.TryParse(arg, out bool value))
{
- Log.LogErrorAndExit($"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/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/DocsPortingTool/Docs/DocsAPI.cs b/Libraries/Docs/DocsAPI.cs
similarity index 70%
rename from DocsPortingTool/Docs/DocsAPI.cs
rename to Libraries/Docs/DocsAPI.cs
index 9b7b19f..f29246d 100644
--- a/DocsPortingTool/Docs/DocsAPI.cs
+++ b/Libraries/Docs/DocsAPI.cs
@@ -4,9 +4,9 @@
using System.Linq;
using System.Xml.Linq;
-namespace DocsPortingTool.Docs
+namespace Libraries.Docs
{
- public abstract class DocsAPI : IDocsAPI
+ internal abstract class DocsAPI : IDocsAPI
{
private string? _docIdEscaped = null;
private List? _params;
@@ -14,6 +14,9 @@ public abstract class DocsAPI : IDocsAPI
private List? _typeParameters;
private List? _typeParams;
private List? _assemblyInfos;
+ private List? _seeAlsoCrefs;
+ private List? _altMemberCrefs;
+ private List? _relateds;
protected readonly XElement XERoot;
@@ -32,14 +35,14 @@ public List Parameters
{
if (_parameters == null)
{
- XElement xeParameters = XERoot.Element("Parameters");
- if (xeParameters != null)
+ XElement? xeParameters = XERoot.Element("Parameters");
+ if (xeParameters == null)
{
- _parameters = xeParameters.Elements("Parameter").Select(x => new DocsParameter(x)).ToList();
+ _parameters = new();
}
else
{
- _parameters = new List();
+ _parameters = xeParameters.Elements("Parameter").Select(x => new DocsParameter(x)).ToList();
}
}
return _parameters;
@@ -55,14 +58,14 @@ public List TypeParameters
{
if (_typeParameters == null)
{
- XElement xeTypeParameters = XERoot.Element("TypeParameters");
- if (xeTypeParameters != null)
+ XElement? xeTypeParameters = XERoot.Element("TypeParameters");
+ if (xeTypeParameters == null)
{
- _typeParameters = xeTypeParameters.Elements("TypeParameter").Select(x => new DocsTypeParameter(x)).ToList();
+ _typeParameters = new();
}
else
{
- _typeParameters = new List();
+ _typeParameters = xeTypeParameters.Elements("TypeParameter").Select(x => new DocsTypeParameter(x)).ToList();
}
}
return _typeParameters;
@@ -73,7 +76,7 @@ public XElement Docs
{
get
{
- return XERoot.Element("Docs");
+ return XERoot.Element("Docs") ?? throw new NullReferenceException($"Docs section was null in {FilePath}");
}
}
@@ -114,13 +117,70 @@ public List TypeParams
}
else
{
- _typeParams = new List();
+ _typeParams = new();
}
}
return _typeParams;
}
}
+ public List SeeAlsoCrefs
+ {
+ get
+ {
+ if (_seeAlsoCrefs == null)
+ {
+ if (Docs != null)
+ {
+ _seeAlsoCrefs = Docs.Elements("seealso").Select(x => XmlHelper.GetAttributeValue(x, "cref")).ToList();
+ }
+ else
+ {
+ _seeAlsoCrefs = new();
+ }
+ }
+ 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;
+ }
+ }
+
public abstract string Summary { get; set; }
public abstract string Remarks { get; set; }
@@ -149,11 +209,11 @@ public string DocIdEscaped
}
}
- public DocsParam SaveParam(XElement xeTripleSlashParam)
+ public DocsParam SaveParam(XElement xeIntelliSenseXmlParam)
{
- XElement xeDocsParam = new XElement(xeTripleSlashParam.Name);
- xeDocsParam.ReplaceAttributes(xeTripleSlashParam.Attributes());
- XmlHelper.SaveFormattedAsXml(xeDocsParam, xeTripleSlashParam.Value);
+ 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;
@@ -220,6 +280,13 @@ protected void SaveFormattedAsMarkdown(string name, string value, bool addIfMiss
// 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)
diff --git a/DocsPortingTool/Docs/DocsAssemblyInfo.cs b/Libraries/Docs/DocsAssemblyInfo.cs
similarity index 93%
rename from DocsPortingTool/Docs/DocsAssemblyInfo.cs
rename to Libraries/Docs/DocsAssemblyInfo.cs
index 81f4180..7840315 100644
--- a/DocsPortingTool/Docs/DocsAssemblyInfo.cs
+++ b/Libraries/Docs/DocsAssemblyInfo.cs
@@ -2,9 +2,9 @@
using System.Linq;
using System.Xml.Linq;
-namespace DocsPortingTool.Docs
+namespace Libraries.Docs
{
- public class DocsAssemblyInfo
+ internal class DocsAssemblyInfo
{
private readonly XElement XEAssemblyInfo;
public string AssemblyName
diff --git a/DocsPortingTool/Docs/DocsAttribute.cs b/Libraries/Docs/DocsAttribute.cs
similarity index 90%
rename from DocsPortingTool/Docs/DocsAttribute.cs
rename to Libraries/Docs/DocsAttribute.cs
index b4111b4..a07ad42 100644
--- a/DocsPortingTool/Docs/DocsAttribute.cs
+++ b/Libraries/Docs/DocsAttribute.cs
@@ -1,8 +1,8 @@
using System.Xml.Linq;
-namespace DocsPortingTool.Docs
+namespace Libraries.Docs
{
- public class DocsAttribute
+ internal class DocsAttribute
{
private readonly XElement XEAttribute;
diff --git a/DocsPortingTool/Docs/DocsCommentsContainer.cs b/Libraries/Docs/DocsCommentsContainer.cs
similarity index 94%
rename from DocsPortingTool/Docs/DocsCommentsContainer.cs
rename to Libraries/Docs/DocsCommentsContainer.cs
index 6bc2ab8..958ee61 100644
--- a/DocsPortingTool/Docs/DocsCommentsContainer.cs
+++ b/Libraries/Docs/DocsCommentsContainer.cs
@@ -6,9 +6,9 @@
using System.Xml;
using System.Xml.Linq;
-namespace DocsPortingTool.Docs
+namespace Libraries.Docs
{
- public class DocsCommentsContainer
+ internal class DocsCommentsContainer
{
private Configuration Config { get; set; }
@@ -105,7 +105,7 @@ public void Save()
private bool HasAllowedDirName(DirectoryInfo dirInfo)
{
- return !Configuration.ForbiddenDirectories.Contains(dirInfo.Name) && !dirInfo.Name.EndsWith(".Tests");
+ return !Configuration.ForbiddenBinSubdirectories.Contains(dirInfo.Name) && !dirInfo.Name.EndsWith(".Tests");
}
private bool HasAllowedFileName(FileInfo fileInfo)
@@ -152,7 +152,7 @@ private IEnumerable EnumerateFiles()
// 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) &&
+ 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"))
{
@@ -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);
@@ -186,7 +185,7 @@ private void LoadFile(FileInfo fileInfo)
return;
}
- DocsType docsType = new DocsType(fileInfo.FullName, xDoc, xDoc.Root);
+ DocsType docsType = new DocsType(fileInfo.FullName, xDoc, xDoc.Root!);
bool add = false;
bool addedAsInterface = false;
@@ -239,7 +238,7 @@ private void LoadFile(FileInfo fileInfo)
int totalMembersAdded = 0;
Types.Add(docsType);
- if (XmlHelper.TryGetChildElement(xDoc.Root, "Members", out XElement? xeMembers) && xeMembers != null)
+ if (XmlHelper.TryGetChildElement(xDoc.Root!, "Members", out XElement? xeMembers) && xeMembers != null)
{
foreach (XElement xeMember in xeMembers.Elements("Member"))
{
@@ -265,8 +264,13 @@ private void LoadFile(FileInfo fileInfo)
}
}
- private bool IsXmlMalformed(XDocument xDoc, string fileName)
+ 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}");
diff --git a/DocsPortingTool/Docs/DocsException.cs b/Libraries/Docs/DocsException.cs
similarity index 84%
rename from DocsPortingTool/Docs/DocsException.cs
rename to Libraries/Docs/DocsException.cs
index 68efc75..1450887 100644
--- a/DocsPortingTool/Docs/DocsException.cs
+++ b/Libraries/Docs/DocsException.cs
@@ -1,10 +1,11 @@
-using System;
+#nullable enable
+using System;
using System.Collections.Generic;
using System.Xml.Linq;
-namespace DocsPortingTool.Docs
+namespace Libraries.Docs
{
- public class DocsException
+ internal class DocsException
{
private readonly XElement XEException;
@@ -48,19 +49,19 @@ public void AppendException(string toAppend)
ParentAPI.Changed = true;
}
- public bool WordCountCollidesAboveThreshold(string tripleSlashValue, int threshold)
+ public bool WordCountCollidesAboveThreshold(string intelliSenseXmlValue, int threshold)
{
- Dictionary hashTripleSlash = GetHash(tripleSlashValue);
+ Dictionary hashIntelliSenseXml = GetHash(intelliSenseXmlValue);
Dictionary hashDocs = GetHash(Value);
int collisions = 0;
- // Iterate all the words of the triple slash exception string
- foreach (KeyValuePair word in hashTripleSlash)
+ // 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 triple slash
+ // 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)
{
@@ -71,7 +72,7 @@ public bool WordCountCollidesAboveThreshold(string tripleSlashValue, int thresho
// 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);
+ double collisionPercentage = (collisions * 100 / (double)hashIntelliSenseXml.Count);
return collisionPercentage >= threshold;
}
diff --git a/DocsPortingTool/Docs/DocsMember.cs b/Libraries/Docs/DocsMember.cs
similarity index 81%
rename from DocsPortingTool/Docs/DocsMember.cs
rename to Libraries/Docs/DocsMember.cs
index baedc1d..e6345d7 100644
--- a/DocsPortingTool/Docs/DocsMember.cs
+++ b/Libraries/Docs/DocsMember.cs
@@ -3,14 +3,13 @@
using System.Linq;
using System.Xml.Linq;
-namespace DocsPortingTool.Docs
+namespace Libraries.Docs
{
- public class DocsMember : DocsAPI
+ 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)
@@ -64,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;
}
@@ -85,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;
}
}
@@ -98,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");
@@ -146,7 +140,7 @@ public override string Remarks
}
set
{
- SaveFormattedAsMarkdown("remarks", value, addIfMissing: !Analyzer.IsEmpty(value), isMember: true);
+ SaveFormattedAsMarkdown("remarks", value, addIfMissing: !value.IsDocsEmpty(), isMember: true);
}
}
@@ -169,25 +163,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/DocsPortingTool/Docs/DocsMemberSignature.cs b/Libraries/Docs/DocsMemberSignature.cs
similarity index 89%
rename from DocsPortingTool/Docs/DocsMemberSignature.cs
rename to Libraries/Docs/DocsMemberSignature.cs
index 3fbdf66..f9eff57 100644
--- a/DocsPortingTool/Docs/DocsMemberSignature.cs
+++ b/Libraries/Docs/DocsMemberSignature.cs
@@ -1,8 +1,8 @@
using System.Xml.Linq;
-namespace DocsPortingTool.Docs
+namespace Libraries.Docs
{
- public class DocsMemberSignature
+ internal class DocsMemberSignature
{
private readonly XElement XEMemberSignature;
diff --git a/DocsPortingTool/Docs/DocsParam.cs b/Libraries/Docs/DocsParam.cs
similarity index 93%
rename from DocsPortingTool/Docs/DocsParam.cs
rename to Libraries/Docs/DocsParam.cs
index 7ec3a1a..c5a09b2 100644
--- a/DocsPortingTool/Docs/DocsParam.cs
+++ b/Libraries/Docs/DocsParam.cs
@@ -1,8 +1,8 @@
using System.Xml.Linq;
-namespace DocsPortingTool.Docs
+namespace Libraries.Docs
{
- public class DocsParam
+ internal class DocsParam
{
private readonly XElement XEDocsParam;
public IDocsAPI ParentAPI
diff --git a/DocsPortingTool/Docs/DocsParameter.cs b/Libraries/Docs/DocsParameter.cs
similarity index 89%
rename from DocsPortingTool/Docs/DocsParameter.cs
rename to Libraries/Docs/DocsParameter.cs
index c9dd4bf..ec598b8 100644
--- a/DocsPortingTool/Docs/DocsParameter.cs
+++ b/Libraries/Docs/DocsParameter.cs
@@ -1,8 +1,8 @@
using System.Xml.Linq;
-namespace DocsPortingTool.Docs
+namespace Libraries.Docs
{
- public class DocsParameter
+ internal class DocsParameter
{
private readonly XElement XEParameter;
public string Name
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/DocsPortingTool/Docs/DocsType.cs b/Libraries/Docs/DocsType.cs
similarity index 81%
rename from DocsPortingTool/Docs/DocsType.cs
rename to Libraries/Docs/DocsType.cs
index 5628635..6c89ccf 100644
--- a/DocsPortingTool/Docs/DocsType.cs
+++ b/Libraries/Docs/DocsType.cs
@@ -3,12 +3,12 @@
using System.Linq;
using System.Xml.Linq;
-namespace DocsPortingTool.Docs
+namespace Libraries.Docs
{
///
/// Represents the root xml element (unique) of a Docs xml file, called Type.
///
- public class DocsType : DocsAPI
+ internal class DocsType : DocsAPI
{
private string? _name;
private string? _fullName;
@@ -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;
}
@@ -99,7 +98,7 @@ public override string DocId
}
}
- public XElement Base
+ public XElement? Base
{
get
{
@@ -111,7 +110,11 @@ public string BaseTypeName
{
get
{
- if (_baseTypeName == null)
+ if (Base == null)
+ {
+ _baseTypeName = string.Empty;
+ }
+ else if (_baseTypeName == null)
{
_baseTypeName = XmlHelper.GetChildElementValue(Base, "BaseTypeName");
}
@@ -119,7 +122,7 @@ public string BaseTypeName
}
}
- public XElement Interfaces
+ public XElement? Interfaces
{
get
{
@@ -131,7 +134,11 @@ public List InterfaceNames
{
get
{
- if (_interfaceNames == null)
+ if (Interfaces == null)
+ {
+ _interfaceNames = new();
+ }
+ else if (_interfaceNames == null)
{
_interfaceNames = Interfaces.Elements("Interface").Select(x => XmlHelper.GetChildElementValue(x, "InterfaceName")).ToList();
}
@@ -145,8 +152,15 @@ public List Attributes
{
if (_attributes == null)
{
- XElement e = XERoot.Element("Attributes");
- _attributes = (e != null) ? e.Elements("Attribute").Select(x => new DocsAttribute(x)).ToList() : new List();
+ 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;
}
@@ -172,7 +186,7 @@ public override string Remarks
}
set
{
- SaveFormattedAsMarkdown("remarks", value, addIfMissing: !Analyzer.IsEmpty(value), isMember: false);
+ SaveFormattedAsMarkdown("remarks", value, addIfMissing: !value.IsDocsEmpty(), isMember: false);
}
}
diff --git a/DocsPortingTool/Docs/DocsTypeParam.cs b/Libraries/Docs/DocsTypeParam.cs
similarity index 91%
rename from DocsPortingTool/Docs/DocsTypeParam.cs
rename to Libraries/Docs/DocsTypeParam.cs
index 08790e3..af2ea7a 100644
--- a/DocsPortingTool/Docs/DocsTypeParam.cs
+++ b/Libraries/Docs/DocsTypeParam.cs
@@ -1,18 +1,19 @@
-using System.Threading;
+#nullable enable
using System.Xml.Linq;
-namespace DocsPortingTool.Docs
+namespace Libraries.Docs
{
///
/// Each one of these typeparam objects live inside the Docs section inside the Member object.
///
- public class DocsTypeParam
+ internal class DocsTypeParam
{
private readonly XElement XEDocsTypeParam;
public IDocsAPI ParentAPI
{
get; private set;
}
+
public string Name
{
get
@@ -20,6 +21,7 @@ public string Name
return XmlHelper.GetAttributeValue(XEDocsTypeParam, "name");
}
}
+
public string Value
{
get
diff --git a/DocsPortingTool/Docs/DocsTypeParameter.cs b/Libraries/Docs/DocsTypeParameter.cs
similarity index 96%
rename from DocsPortingTool/Docs/DocsTypeParameter.cs
rename to Libraries/Docs/DocsTypeParameter.cs
index d32482e..ac66251 100644
--- a/DocsPortingTool/Docs/DocsTypeParameter.cs
+++ b/Libraries/Docs/DocsTypeParameter.cs
@@ -2,12 +2,12 @@
using System.Linq;
using System.Xml.Linq;
-namespace DocsPortingTool.Docs
+namespace Libraries.Docs
{
///
/// Each one of these TypeParameter objects islocated inside the TypeParameters section inside the Member.
///
- public class DocsTypeParameter
+ internal class DocsTypeParameter
{
private readonly XElement XETypeParameter;
public string Name
diff --git a/DocsPortingTool/Docs/DocsTypeSignature.cs b/Libraries/Docs/DocsTypeSignature.cs
similarity index 89%
rename from DocsPortingTool/Docs/DocsTypeSignature.cs
rename to Libraries/Docs/DocsTypeSignature.cs
index 97fa67c..5ca5c46 100644
--- a/DocsPortingTool/Docs/DocsTypeSignature.cs
+++ b/Libraries/Docs/DocsTypeSignature.cs
@@ -1,8 +1,8 @@
using System.Xml.Linq;
-namespace DocsPortingTool.Docs
+namespace Libraries.Docs
{
- public class DocsTypeSignature
+ internal class DocsTypeSignature
{
private readonly XElement XETypeSignature;
diff --git a/DocsPortingTool/Docs/IDocsAPI.cs b/Libraries/Docs/IDocsAPI.cs
similarity index 93%
rename from DocsPortingTool/Docs/IDocsAPI.cs
rename to Libraries/Docs/IDocsAPI.cs
index 71cf16a..837e371 100644
--- a/DocsPortingTool/Docs/IDocsAPI.cs
+++ b/Libraries/Docs/IDocsAPI.cs
@@ -1,9 +1,9 @@
using System.Collections.Generic;
using System.Xml.Linq;
-namespace DocsPortingTool.Docs
+namespace Libraries.Docs
{
- public interface IDocsAPI
+ internal interface IDocsAPI
{
public abstract APIKind Kind { get; }
public abstract bool Changed { get; set; }
diff --git a/DocsPortingTool/Extensions.cs b/Libraries/Extensions.cs
similarity index 77%
rename from DocsPortingTool/Extensions.cs
rename to Libraries/Extensions.cs
index fdcb90d..03966e4 100644
--- a/DocsPortingTool/Extensions.cs
+++ b/Libraries/Extensions.cs
@@ -1,9 +1,11 @@
-using System.Collections.Generic;
+#nullable enable
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
-namespace DocsPortingTool
+namespace Libraries
{
// Provides generic extension methods.
- public static class Extensions
+ 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)
@@ -32,6 +34,10 @@ public static string RemoveSubstrings(this string oldString, params string[] str
// 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/DocsPortingTool/TripleSlash/TripleSlashCommentsContainer.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs
similarity index 60%
rename from DocsPortingTool/TripleSlash/TripleSlashCommentsContainer.cs
rename to Libraries/IntelliSenseXml/IntelliSenseXmlCommentsContainer.cs
index 8223a48..e6b5a55 100644
--- a/DocsPortingTool/TripleSlash/TripleSlashCommentsContainer.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;
@@ -6,7 +7,7 @@
using System.Xml.Linq;
/*
-The triple slash comments xml files for...
+The IntelliSense xml comments files for...
A) corefx are saved in:
corefx/artifacts/bin/
B) coreclr are saved in:
@@ -29,50 +30,43 @@ Each xml file represents a namespace.
exception (0:M)
Note: The exception value may contain xml nodes.
*/
-namespace DocsPortingTool.TripleSlash
+namespace Libraries.IntelliSenseXml
{
- public class TripleSlashCommentsContainer
+ internal class IntelliSenseXmlCommentsContainer
{
private Configuration Config { get; set; }
private XDocument? xDoc = null;
- public List Members = new List();
+ // 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 int TotalFiles
- {
- get
- {
- return Members.Count;
- }
- }
-
- public TripleSlashCommentsContainer(Configuration config)
+ public IntelliSenseXmlCommentsContainer(Configuration config)
{
Config = config;
}
public void CollectFiles()
{
- Log.Info("Looking for triple slash xml files...");
+ Log.Info("Looking for IntelliSense xml files...");
foreach (FileInfo fileInfo in EnumerateFiles())
{
LoadFile(fileInfo, printSuccess: true);
}
- Log.Success("Finished looking for triple slash xml files.");
+ Log.Success("Finished looking for IntelliSense xml files.");
Log.Line();
}
private IEnumerable EnumerateFiles()
{
- foreach (DirectoryInfo dirInfo in Config.DirsTripleSlashXmls)
+ foreach (DirectoryInfo dirInfo in Config.DirsIntelliSense)
{
- // 1) Find all the xml files inside all the subdirectories inside the triple slash xml directory
+ // 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.ForbiddenDirectories.Contains(subDir.Name) && !subDir.Name.EndsWith(".Tests"))
+ if (!Configuration.ForbiddenBinSubdirectories.Contains(subDir.Name) && !subDir.Name.EndsWith(".Tests"))
{
foreach (FileInfo fileInfo in subDir.EnumerateFiles("*.xml", SearchOption.AllDirectories))
{
@@ -93,23 +87,22 @@ private void LoadFile(FileInfo fileInfo, bool printSuccess)
{
if (!fileInfo.Exists)
{
- Log.Error($"Triple slash xml file does not exist: {fileInfo.FullName}");
- return;
+ throw new Exception($"The IntelliSense xml file does not exist: {fileInfo.FullName}");
}
xDoc = XDocument.Load(fileInfo.FullName);
- if (IsXmlMalformed(xDoc, fileInfo.FullName, out string? assembly))
+ if (TryGetAssemblyName(xDoc, fileInfo.FullName, out string? assembly))
{
return;
}
int totalAdded = 0;
- if (XmlHelper.TryGetChildElement(xDoc.Root, "members", out XElement? xeMembers) && xeMembers != null)
+ if (XmlHelper.TryGetChildElement(xDoc.Root!, "members", out XElement? xeMembers) && xeMembers != null)
{
foreach (XElement xeMember in xeMembers.Elements("member"))
{
- TripleSlashMember member = new TripleSlashMember(xeMember, assembly);
+ IntelliSenseXmlMember member = new IntelliSenseXmlMember(xeMember, assembly);
if (Config.IncludedAssemblies.Any(included => member.Assembly.StartsWith(included)) &&
!Config.ExcludedAssemblies.Any(excluded => member.Assembly.StartsWith(excluded)))
@@ -128,55 +121,66 @@ private void LoadFile(FileInfo fileInfo, bool printSuccess)
if (printSuccess && totalAdded > 0)
{
- Log.Success($"{totalAdded} triple slash member(s) added from xml file '{fileInfo.FullName}'");
+ Log.Success($"{totalAdded} IntelliSense xml member(s) added from xml file '{fileInfo.FullName}'");
}
}
- private bool IsXmlMalformed(XDocument xDoc, string fileName, [NotNullWhen(returnValue: false)] out string? assembly)
+ // 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($"Triple slash xml file does not contain a root element: {fileName}");
+ Log.Error($"The IntelliSense 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}");
+ Log.Error($"The IntelliSense 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}");
+ Log.Error($"The IntelliSense 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}");
+ 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($"Triple slash xml file does not contain exactly 1 'members' element: {fileName}");
+ Log.Error($"The IntelliSense xml file does not contain exactly 1 'members' element: {fileName}");
return true;
}
- XElement xAssembly = xDoc.Root.Element("assembly");
+ 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($"Triple slash xml file assembly element does not contain exactly 1 'name' element: {fileName}");
+ Log.Error($"The IntelliSense xml file assembly element does not contain exactly 1 'name' element: {fileName}");
return true;
}
- assembly = xAssembly.Element("name").Value;
+ assembly = xAssembly.Element("name")!.Value;
if (string.IsNullOrEmpty(assembly))
{
- Log.Error($"Triple slash xml file assembly string is null or empty: {fileName}");
+ Log.Error($"The IntelliSense xml file assembly string is null or empty: {fileName}");
return true;
}
diff --git a/DocsPortingTool/TripleSlash/TripleSlashException.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlException.cs
similarity index 87%
rename from DocsPortingTool/TripleSlash/TripleSlashException.cs
rename to Libraries/IntelliSenseXml/IntelliSenseXmlException.cs
index 0adba3e..44b3645 100644
--- a/DocsPortingTool/TripleSlash/TripleSlashException.cs
+++ b/Libraries/IntelliSenseXml/IntelliSenseXmlException.cs
@@ -1,8 +1,8 @@
using System.Xml.Linq;
-namespace DocsPortingTool.TripleSlash
+namespace Libraries.IntelliSenseXml
{
- public class TripleSlashException
+ internal class IntelliSenseXmlException
{
public XElement XEException
{
@@ -36,7 +36,7 @@ public string Value
}
}
- public TripleSlashException(XElement xeException)
+ public IntelliSenseXmlException(XElement xeException)
{
XEException = xeException;
}
diff --git a/DocsPortingTool/TripleSlash/TripleSlashMember.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs
similarity index 80%
rename from DocsPortingTool/TripleSlash/TripleSlashMember.cs
rename to Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs
index 1036af0..17157e7 100644
--- a/DocsPortingTool/TripleSlash/TripleSlashMember.cs
+++ b/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs
@@ -3,9 +3,9 @@
using System.Linq;
using System.Xml.Linq;
-namespace DocsPortingTool.TripleSlash
+namespace Libraries.IntelliSenseXml
{
- public class TripleSlashMember
+ internal class IntelliSenseXmlMember
{
private readonly XElement XEMember;
@@ -46,40 +46,40 @@ public string Name
}
}
- private List? _params;
- public List Params
+ private List? _params;
+ public List Params
{
get
{
if (_params == null)
{
- _params = XEMember.Elements("param").Select(x => new TripleSlashParam(x)).ToList();
+ _params = XEMember.Elements("param").Select(x => new IntelliSenseXmlParam(x)).ToList();
}
return _params;
}
}
- private List? _typeParams;
- public List TypeParams
+ private List? _typeParams;
+ public List TypeParams
{
get
{
if (_typeParams == null)
{
- _typeParams = XEMember.Elements("typeparam").Select(x => new TripleSlashTypeParam(x)).ToList();
+ _typeParams = XEMember.Elements("typeparam").Select(x => new IntelliSenseXmlTypeParam(x)).ToList();
}
return _typeParams;
}
}
- private List? _exceptions;
- public IEnumerable Exceptions
+ private List? _exceptions;
+ public IEnumerable Exceptions
{
get
{
if (_exceptions == null)
{
- _exceptions = XEMember.Elements("exception").Select(x => new TripleSlashException(x)).ToList();
+ _exceptions = XEMember.Elements("exception").Select(x => new IntelliSenseXmlException(x)).ToList();
}
return _exceptions;
}
@@ -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,14 +134,14 @@ 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;
}
}
- public TripleSlashMember(XElement xeMember, string assembly)
+ public IntelliSenseXmlMember(XElement xeMember, string assembly)
{
if (string.IsNullOrEmpty(assembly))
{
diff --git a/DocsPortingTool/TripleSlash/TripleSlashParam.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlParam.cs
similarity index 86%
rename from DocsPortingTool/TripleSlash/TripleSlashParam.cs
rename to Libraries/IntelliSenseXml/IntelliSenseXmlParam.cs
index f8ef49f..b5931a1 100644
--- a/DocsPortingTool/TripleSlash/TripleSlashParam.cs
+++ b/Libraries/IntelliSenseXml/IntelliSenseXmlParam.cs
@@ -1,8 +1,8 @@
using System.Xml.Linq;
-namespace DocsPortingTool.TripleSlash
+namespace Libraries.IntelliSenseXml
{
- public class TripleSlashParam
+ internal class IntelliSenseXmlParam
{
public XElement XEParam
{
@@ -36,7 +36,7 @@ public string Value
}
}
- public TripleSlashParam(XElement xeParam)
+ public IntelliSenseXmlParam(XElement xeParam)
{
XEParam = xeParam;
}
diff --git a/DocsPortingTool/TripleSlash/TripleSlashTypeParam.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlTypeParam.cs
similarity index 85%
rename from DocsPortingTool/TripleSlash/TripleSlashTypeParam.cs
rename to Libraries/IntelliSenseXml/IntelliSenseXmlTypeParam.cs
index 510cc95..7fda8f2 100644
--- a/DocsPortingTool/TripleSlash/TripleSlashTypeParam.cs
+++ b/Libraries/IntelliSenseXml/IntelliSenseXmlTypeParam.cs
@@ -1,8 +1,8 @@
using System.Xml.Linq;
-namespace DocsPortingTool.TripleSlash
+namespace Libraries.IntelliSenseXml
{
- public class TripleSlashTypeParam
+ internal class IntelliSenseXmlTypeParam
{
public XElement XETypeParam;
@@ -32,7 +32,7 @@ public string Value
}
}
- public TripleSlashTypeParam(XElement xeTypeParam)
+ public IntelliSenseXmlTypeParam(XElement xeTypeParam)
{
XETypeParam = xeTypeParam;
}
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/DocsPortingTool/Log.cs b/Libraries/Log.cs
similarity index 73%
rename from DocsPortingTool/Log.cs
rename to Libraries/Log.cs
index 3c29b9a..f98cd79 100644
--- a/DocsPortingTool/Log.cs
+++ b/Libraries/Log.cs
@@ -1,8 +1,9 @@
-using System;
+#nullable enable
+using System;
-namespace DocsPortingTool
+namespace Libraries
{
- public class Log
+ internal class Log
{
private static void WriteLine(string format, params object[]? args)
{
@@ -133,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);
@@ -146,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);
}
}
@@ -157,17 +164,15 @@ public static void Line()
public delegate void PrintHelpFunction();
- public static void LogErrorAndExit(string format, params object[]? args)
+ public static void PrintHelpAndError(string format, params object[]? args)
{
+ PrintHelp();
Error(format, args);
- Environment.Exit(0);
- }
- public static void LogErrorPrintHelpAndExit(string format, params object[]? args)
- {
- Error(format, args);
- PrintHelp();
- Environment.Exit(0);
+ if (args == null)
+ throw new Exception(format);
+ else
+ throw new Exception(string.Format(format, args));
}
public static void PrintHelp()
@@ -179,47 +184,79 @@ The instructions below assume %SourceRepos% is the root folder of all your git c
Options:
-
MANDATORY
------------------------------------------------------------
| PARAMETER | TYPE | DESCRIPTION |
------------------------------------------------------------
- -Docs folder path The absolute directory root path where the Docs xml files are located.
- Known locations:
+ -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,%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.
+ -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\.tools\native\bin\dotnet-api-docs_netcoreapp3.0\0.0.0.1\_intellisense\netcore-3.0\
+ > WPF: %SourceRepos%\wpf\artifacts\bin\
Usage example:
- -TripleSlash %SourceRepos%\corefx\artifacts\bin\,%SourceRepos%\winforms\artifacts\bin\
+ -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.
+ 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, nothing else is processed and the program exits.
+ -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.
+ Using this option automatically sets `SkipInterfaceImplementations` to `true`, to avoid loading
+ unnecessary interface docs xml files into memory.
+ Usage example:
+ -Direction ToTripleSlash
-DisablePrompts bool Default is false (prompts are disabled).
Avoids prompting the user for input to correct some particular errors.
@@ -228,9 +265,9 @@ The instructions below assume %SourceRepos% is the root folder of all your git c
-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.
+ 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 Triple Slash string is appended at the end of the Docs string.
+ 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
@@ -353,17 +390,17 @@ the interface API.
");
Warning(@"
- tl;dr: Just specify these parameters:
+ tl;dr: To port from IntelliSense xmls to DOcs, specify these parameters:
-Docs
- -TripleSlash [,,...,]
+ -IntelliSense [,,...,]
-IncludedAssemblies [,,...]
-Save true
Example:
DocsPortingTool \
-Docs D:\dotnet-api-docs\xml \
- -TripleSlash D:\runtime\artifacts\bin \
+ -IntelliSense D:\runtime\artifacts\bin\System.IO.FileSystem\ \
-IncludedAssemblies System.IO.FileSystem,System.Runtime.Intrinsics \
-Save true
");
diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs
new file mode 100644
index 0000000..4ea8b26
--- /dev/null
+++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs
@@ -0,0 +1,736 @@
+#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;
+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) ->
+ XmlName (XmlNameSyntax) -> 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 ->
+ XmlName -> 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 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; }
+
+ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticModel model) : base(visitIntoStructuredTrivia: true)
+ {
+ DocsComments = docsComments;
+ Model = model;
+ }
+
+ #region Visitor overrides
+
+ 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? VisitDelegateDeclaration(DelegateDeclarationSyntax node)
+ {
+ SyntaxNode? baseNode = base.VisitDelegateDeclaration(node);
+
+ ISymbol? symbol = Model.GetDeclaredSymbol(node);
+ if (symbol == null)
+ {
+ Log.Warning($"Symbol is null.");
+ return baseNode;
+ }
+
+ return VisitType(baseNode, symbol);
+ }
+
+ public override SyntaxNode? VisitEnumDeclaration(EnumDeclarationSyntax node) =>
+ VisitMemberDeclaration(node);
+
+ public override SyntaxNode? VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) =>
+ VisitMemberDeclaration(node);
+
+ public override SyntaxNode? VisitEventFieldDeclaration(EventFieldDeclarationSyntax node) =>
+ VisitVariableDeclaration(node);
+
+ public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node) =>
+ VisitVariableDeclaration(node);
+
+ public override SyntaxNode? VisitInterfaceDeclaration(InterfaceDeclarationSyntax node)
+ {
+ 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? VisitOperatorDeclaration(OperatorDeclarationSyntax node) =>
+ VisitBaseMethodDeclaration(node);
+
+ public override SyntaxNode? VisitPropertyDeclaration(PropertyDeclarationSyntax node)
+ {
+ if (!TryGetMember(node, out DocsMember? member))
+ {
+ return node;
+ }
+
+ SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node);
+
+ 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);
+ SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace);
+
+ 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);
+
+ ISymbol? symbol = Model.GetDeclaredSymbol(node);
+ if (symbol == null)
+ {
+ Log.Warning($"Symbol is null.");
+ return baseNode;
+ }
+
+ return VisitType(baseNode, symbol);
+ }
+
+ #endregion
+
+ #region Visit helpers
+
+ 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;
+ }
+
+ SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node);
+
+ if (!TryGetType(symbol, out DocsType? type))
+ {
+ return node;
+ }
+
+
+ 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);
+ SyntaxTriviaList altmembers = GetAltMembers(type.AltMembers, leadingWhitespace);
+ SyntaxTriviaList relateds = GetRelateds(type.Relateds, leadingWhitespace);
+
+
+ return GetNodeWithTrivia(leadingWhitespace, node, summary, parameters, typeParameters, remarks, seealsos, altmembers, relateds);
+ }
+
+ private SyntaxNode? VisitBaseMethodDeclaration(BaseMethodDeclarationSyntax node)
+ {
+ // The Docs files only contain docs for public elements,
+ // so if no comments are found, we return the node unmodified
+ if (!TryGetMember(node, out DocsMember? member))
+ {
+ return node;
+ }
+
+ SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node);
+
+ SyntaxTriviaList summary = GetSummary(member, leadingWhitespace);
+ SyntaxTriviaList parameters = GetParameters(member, leadingWhitespace);
+ SyntaxTriviaList typeParameters = GetTypeParameters(member, 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);
+ SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace);
+
+ return GetNodeWithTrivia(leadingWhitespace, node, summary, parameters, typeParameters, returns, remarks, exceptions, seealsos, altmembers, relateds);
+ }
+
+ private SyntaxNode? VisitMemberDeclaration(MemberDeclarationSyntax node)
+ {
+ if (!TryGetMember(node, out DocsMember? member))
+ {
+ return node;
+ }
+
+ SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node);
+
+ 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);
+ SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace);
+
+ 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(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);
+
+ return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks, seealsos, altmembers, relateds);
+ }
+
+ 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();
+ foreach (SyntaxTriviaList t in trivias)
+ {
+ finalTrivia = finalTrivia.AddRange(t);
+ }
+ if (finalTrivia.Count > 0)
+ {
+ finalTrivia = finalTrivia.AddRange(leadingWhitespace);
+
+ var leadingTrivia = node.GetLeadingTrivia();
+ if (leadingTrivia.Any())
+ {
+ if (leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia))
+ {
+ // Ensure the endline that separates nodes is respected
+ finalTrivia = new SyntaxTriviaList(SyntaxFactory.ElasticCarriageReturnLineFeed)
+ .AddRange(finalTrivia);
+ }
+ }
+
+ return node.WithLeadingTrivia(finalTrivia);
+ }
+
+ // If there was no new trivia, return untouched
+ return node;
+ }
+
+ // Finds the last set of whitespace characters that are to the left of the public|protected keyword of the node.
+ private static SyntaxTriviaList GetLeadingWhitespace(SyntaxNode node)
+ {
+ 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 static SyntaxTriviaList GetSummary(DocsAPI api, SyntaxTriviaList leadingWhitespace)
+ {
+ if (!api.Summary.IsDocsEmpty())
+ {
+ XmlTextSyntax contents = GetTextAsCommentedTokens(api.Summary, leadingWhitespace);
+ XmlElementSyntax element = SyntaxFactory.XmlSummaryElement(contents);
+ return GetXmlTrivia(element, leadingWhitespace);
+ }
+
+ return new();
+ }
+
+ private static SyntaxTriviaList GetRemarks(DocsAPI api, SyntaxTriviaList leadingWhitespace)
+ {
+ if (!api.Remarks.IsDocsEmpty())
+ {
+ string text = GetRemarksWithXmlElements(api);
+ XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace);
+ XmlElementSyntax xmlRemarks = SyntaxFactory.XmlRemarksElement(contents);
+ return GetXmlTrivia(xmlRemarks, leadingWhitespace);
+ }
+
+ return new();
+ }
+
+ private static SyntaxTriviaList GetValue(DocsMember api, SyntaxTriviaList 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 static SyntaxTriviaList GetParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace)
+ {
+ SyntaxTriviaList parameters = new();
+ foreach (SyntaxTriviaList parameterTrivia in api.Params
+ .Where(param => !param.Value.IsDocsEmpty())
+ .Select(param => GetParameter(param.Name, param.Value, leadingWhitespace)))
+ {
+ parameters = parameters.AddRange(parameterTrivia);
+ }
+ return parameters;
+ }
+
+ private static SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace)
+ {
+ if (!text.IsDocsEmpty())
+ {
+ var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute("name", name));
+ XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace);
+ return GetXmlTrivia("typeparam", attribute, contents, leadingWhitespace);
+ }
+
+ return new();
+ }
+
+ private static SyntaxTriviaList GetTypeParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace)
+ {
+ SyntaxTriviaList typeParameters = new();
+ foreach (SyntaxTriviaList typeParameterTrivia in api.TypeParams
+ .Where(typeParam => !typeParam.Value.IsDocsEmpty())
+ .Select(typeParam => GetTypeParam(typeParam.Name, typeParam.Value, leadingWhitespace)))
+ {
+ typeParameters = typeParameters.AddRange(typeParameterTrivia);
+ }
+ return typeParameters;
+ }
+
+ private static SyntaxTriviaList GetReturns(DocsMember api, SyntaxTriviaList leadingWhitespace)
+ {
+ // Also applies for when is empty because the method return type is void
+ if (!api.Returns.IsDocsEmpty())
+ {
+ XmlTextSyntax contents = GetTextAsCommentedTokens(api.Returns, leadingWhitespace);
+ XmlElementSyntax element = SyntaxFactory.XmlReturnsElement(contents);
+ return GetXmlTrivia(element, leadingWhitespace);
+ }
+
+ return new();
+ }
+
+ private static SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace)
+ {
+ if (!text.IsDocsEmpty())
+ {
+ TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(RemoveDocIdPrefixes(cref)));
+ XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace);
+ XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents);
+ return GetXmlTrivia(element, leadingWhitespace);
+ }
+
+ return new();
+ }
+
+ private static SyntaxTriviaList GetExceptions(List docsExceptions, SyntaxTriviaList leadingWhitespace)
+ {
+ SyntaxTriviaList exceptions = new();
+ if (docsExceptions.Any())
+ {
+ foreach (SyntaxTriviaList exceptionsTrivia in docsExceptions.Select(
+ exception => GetException(exception.Cref, exception.Value, leadingWhitespace)))
+ {
+ exceptions = exceptions.AddRange(exceptionsTrivia);
+ }
+ }
+ return exceptions;
+ }
+
+ private static SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace)
+ {
+ cref = ReplacePrimitiveTypes(cref);
+ TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref));
+ XmlEmptyElementSyntax element = SyntaxFactory.XmlSeeAlsoElement(crefSyntax);
+ return GetXmlTrivia(element, leadingWhitespace);
+ }
+
+ private static SyntaxTriviaList GetSeeAlsos(List docsSeeAlsoCrefs, SyntaxTriviaList leadingWhitespace)
+ {
+ SyntaxTriviaList seealsos = new();
+ if (docsSeeAlsoCrefs.Any())
+ {
+ foreach (SyntaxTriviaList seealsoTrivia in docsSeeAlsoCrefs.Select(
+ s => GetSeeAlso(s, leadingWhitespace)))
+ {
+ seealsos = seealsos.AddRange(seealsoTrivia);
+ }
+ }
+ return seealsos;
+ }
+
+ private static SyntaxTriviaList GetAltMember(string cref, SyntaxTriviaList leadingWhitespace)
+ {
+ cref = ReplacePrimitiveTypes(cref);
+ XmlAttributeSyntax attribute = SyntaxFactory.XmlTextAttribute("cref", cref);
+ XmlEmptyElementSyntax emptyElement = SyntaxFactory.XmlEmptyElement(SyntaxFactory.XmlName(SyntaxFactory.Identifier("altmember")), new SyntaxList(attribute));
+ return GetXmlTrivia(emptyElement, leadingWhitespace);
+ }
+
+ private static SyntaxTriviaList GetAltMembers(List docsAltMembers, SyntaxTriviaList leadingWhitespace)
+ {
+ SyntaxTriviaList altMembers = new();
+ if (docsAltMembers.Any())
+ {
+ foreach (SyntaxTriviaList altMemberTrivia in docsAltMembers.Select(
+ s => GetAltMember(s, leadingWhitespace)))
+ {
+ altMembers = altMembers.AddRange(altMemberTrivia);
+ }
+ }
+ return altMembers;
+ }
+
+ 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));
+
+ XmlTextSyntax contents = GetTextAsCommentedTokens(value, leadingWhitespace);
+ return GetXmlTrivia("related", attributes, contents, leadingWhitespace);
+ }
+
+ private static SyntaxTriviaList GetRelateds(List docsRelateds, SyntaxTriviaList leadingWhitespace)
+ {
+ SyntaxTriviaList relateds = new();
+ if (docsRelateds.Any())
+ {
+ foreach (SyntaxTriviaList relatedsTrivia in docsRelateds.Select(
+ s => GetRelated(s.ArticleType, s.Href, s.Value, leadingWhitespace)))
+ {
+ relateds = relateds.AddRange(relatedsTrivia);
+ }
+ }
+ return relateds;
+ }
+
+ private static XmlTextSyntax GetTextAsCommentedTokens(string text, SyntaxTriviaList leadingWhitespace)
+ {
+ text = ReplacePrimitiveTypes(text);
+
+ // 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[] lines = text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ for (int lineNumber = 0; lineNumber < lines.Length; lineNumber++)
+ {
+ string line = lines[lineNumber];
+
+ SyntaxToken token = SyntaxFactory.XmlTextLiteral(leading, line, line, default);
+ tokens.Add(token);
+
+ if (lines.Length > 1 && lineNumber < lines.Length - 1)
+ {
+ tokens.Add(whitespaceToken);
+ }
+ }
+
+ XmlTextSyntax xmlText = SyntaxFactory.XmlText(tokens.ToArray());
+ return xmlText;
+ }
+
+ private static 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 static SyntaxTriviaList GetXmlTrivia(string name, SyntaxList attributes, XmlTextSyntax contents, SyntaxTriviaList leadingWhitespace)
+ {
+ XmlElementStartTagSyntax start = SyntaxFactory.XmlElementStartTag(
+ SyntaxFactory.Token(SyntaxKind.LessThanToken),
+ SyntaxFactory.XmlName(SyntaxFactory.Identifier(name)),
+ attributes,
+ SyntaxFactory.Token(SyntaxKind.GreaterThanToken));
+
+ XmlElementEndTagSyntax end = SyntaxFactory.XmlElementEndTag(
+ SyntaxFactory.Token(SyntaxKind.LessThanSlashToken),
+ SyntaxFactory.XmlName(SyntaxFactory.Identifier(name)),
+ SyntaxFactory.Token(SyntaxKind.GreaterThanToken));
+
+ XmlElementSyntax element = SyntaxFactory.XmlElement(start, new SyntaxList(contents), end);
+ return GetXmlTrivia(element, leadingWhitespace);
+ }
+
+ 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_,\.\[\]\(\)`\{\}\@\+\*\&\^\#]+)(?%2A)?(?\?[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}", $"");
+ }
+ }
+
+ remarks = ReplacePrimitiveTypes(remarks);
+ }
+ return remarks;
+ }
+
+ private static string RemoveDocIdPrefixes(string text)
+ {
+ if (text.Length > 2 && text[1] == ':')
+ {
+ return text[2..];
+ }
+
+ 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, key, value);
+ }
+ return text;
+ }
+
+ #endregion
+ }
+}
diff --git a/DocsPortingTool/Analyzer.cs b/Libraries/ToDocsPorter.cs
similarity index 83%
rename from DocsPortingTool/Analyzer.cs
rename to Libraries/ToDocsPorter.cs
index 540449a..d74f994 100644
--- a/DocsPortingTool/Analyzer.cs
+++ b/Libraries/ToDocsPorter.cs
@@ -1,15 +1,19 @@
#nullable enable
-using DocsPortingTool.Docs;
-using DocsPortingTool.TripleSlash;
+using Libraries.Docs;
+using Libraries.IntelliSenseXml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
-namespace DocsPortingTool
+namespace Libraries
{
- public class Analyzer
+ 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();
@@ -18,48 +22,44 @@ public class Analyzer
private int TotalModifiedIndividualElements = 0;
- private readonly TripleSlashCommentsContainer TripleSlashComments;
- private readonly DocsCommentsContainer DocsComments;
-
- private Configuration Config { get; set; }
-
- public Analyzer(Configuration config)
+ public ToDocsPorter(Configuration config)
{
+ if (config.Direction != Configuration.PortingDirection.ToDocs)
+ {
+ throw new InvalidOperationException($"Unexpected porting direction: {config.Direction}");
+ }
Config = config;
- TripleSlashComments = new TripleSlashCommentsContainer(config);
DocsComments = new DocsCommentsContainer(config);
+ IntelliSenseXmlComments = new IntelliSenseXmlCommentsContainer(config);
+
}
- // Do all the magic.
public void Start()
{
- TripleSlashComments.CollectFiles();
+ IntelliSenseXmlComments.CollectFiles();
- if (TripleSlashComments.TotalFiles > 0)
+ if (!IntelliSenseXmlComments.Members.Any())
{
- DocsComments.CollectFiles();
- PortMissingComments();
+ Log.Error("No IntelliSense xml comments found.");
}
- else
+
+ DocsComments.CollectFiles();
+ if (!DocsComments.Types.Any())
{
- Log.Error("No triple slash comments found.");
+ Log.Error("No Docs Type APIs found.");
}
+ PortMissingComments();
+
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...");
+ Log.Info("Looking for IntelliSense xml comments that can be ported...");
foreach (DocsType dTypeToUpdate in DocsComments.Types)
{
@@ -72,10 +72,10 @@ private void PortMissingComments()
}
}
- // Tries to find a triple slash element from which to port documentation for the specified Docs type.
+ // Tries to find an IntelliSense xml 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);
+ IntelliSenseXmlMember? tsTypeToPort = IntelliSenseXmlComments.Members.FirstOrDefault(x => x.Name == dTypeToUpdate.DocIdEscaped);
if (tsTypeToPort != null)
{
if (tsTypeToPort.Name == dTypeToUpdate.DocIdEscaped)
@@ -94,11 +94,11 @@ private void PortMissingCommentsForType(DocsType dTypeToUpdate)
}
}
- // Tries to find a triple slash element from which to port documentation for the specified Docs member.
+ // 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;
- TripleSlashMember? tsMemberToPort = TripleSlashComments.Members.FirstOrDefault(x => x.Name == docId);
+ IntelliSenseXmlMember? tsMemberToPort = IntelliSenseXmlComments.Members.FirstOrDefault(x => x.Name == docId);
TryGetEIIMember(dMemberToUpdate, out DocsMember? interfacedMember);
if (tsMemberToPort != null || interfacedMember != null)
@@ -158,7 +158,7 @@ private bool TryGetEIIMember(IDocsAPI dApiToUpdate, out DocsMember? interfacedMe
}
// Ports the summary for the specified API if the field is undocumented.
- private void TryPortMissingSummaryForAPI(IDocsAPI dApiToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember)
+ private void TryPortMissingSummaryForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember)
{
if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeSummaries ||
dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberSummaries)
@@ -167,22 +167,22 @@ private void TryPortMissingSummaryForAPI(IDocsAPI dApiToUpdate, TripleSlashMembe
}
// Only port if undocumented in MS Docs
- if (IsEmpty(dApiToUpdate.Summary))
+ if (dApiToUpdate.Summary.IsDocsEmpty())
{
bool isEII = false;
string name = string.Empty;
string value = string.Empty;
- // Try to port triple slash comments
- if (tsMemberToPort != null && !IsEmpty(tsMemberToPort.Summary))
+ // 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 && !IsEmpty(interfacedMember.Summary))
+ else if (interfacedMember != null && !interfacedMember.Summary.IsDocsEmpty())
{
dApiToUpdate.Summary = interfacedMember.Summary;
isEII = true;
@@ -190,7 +190,7 @@ private void TryPortMissingSummaryForAPI(IDocsAPI dApiToUpdate, TripleSlashMembe
value = interfacedMember.Summary;
}
- if (!IsEmpty(value))
+ if (!value.IsDocsEmpty())
{
// Any member can have an empty summary
string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} summary: {name.Escaped()} = {value.Escaped()}";
@@ -201,7 +201,7 @@ private void TryPortMissingSummaryForAPI(IDocsAPI dApiToUpdate, TripleSlashMembe
}
// Ports the remarks for the specified API if the field is undocumented.
- private void TryPortMissingRemarksForAPI(IDocsAPI dApiToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember, bool skipInterfaceRemarks)
+ private void TryPortMissingRemarksForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember, bool skipInterfaceRemarks)
{
if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeRemarks ||
dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberRemarks)
@@ -209,14 +209,14 @@ private void TryPortMissingRemarksForAPI(IDocsAPI dApiToUpdate, TripleSlashMembe
return;
}
- if (IsEmpty(dApiToUpdate.Remarks))
+ if (dApiToUpdate.Remarks.IsDocsEmpty())
{
bool isEII = false;
string name = string.Empty;
string value = string.Empty;
- // Try to port triple slash comments
- if (tsMemberToPort != null && !IsEmpty(tsMemberToPort.Remarks))
+ // Try to port IntelliSense xml comments
+ if (tsMemberToPort != null && !tsMemberToPort.Remarks.IsDocsEmpty())
{
dApiToUpdate.Remarks = tsMemberToPort.Remarks;
name = tsMemberToPort.Name;
@@ -224,7 +224,7 @@ private void TryPortMissingRemarksForAPI(IDocsAPI dApiToUpdate, TripleSlashMembe
}
// 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))
+ else if (interfacedMember != null && !interfacedMember.Remarks.IsDocsEmpty())
{
DocsMember memberToUpdate = (DocsMember)dApiToUpdate;
@@ -235,12 +235,18 @@ private void TryPortMissingRemarksForAPI(IDocsAPI dApiToUpdate, TripleSlashMembe
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 = interfacedMember.Remarks.RemoveSubstrings("##Remarks", "## Remarks", "");
+ 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 += Environment.NewLine + line;
+ }
}
// Only port the interface remarks if the user desired that
@@ -261,7 +267,7 @@ private void TryPortMissingRemarksForAPI(IDocsAPI dApiToUpdate, TripleSlashMembe
}
}
- if (!IsEmpty(value))
+ if (!value.IsDocsEmpty())
{
// Any member can have an empty remark
string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} remarks: {name.Escaped()} = {value.Escaped()}";
@@ -272,7 +278,7 @@ private void TryPortMissingRemarksForAPI(IDocsAPI dApiToUpdate, TripleSlashMembe
}
// Ports all the parameter descriptions for the specified API if any of them is undocumented.
- private void TryPortMissingParamsForAPI(IDocsAPI dApiToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember)
+ private void TryPortMissingParamsForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember)
{
if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeParams ||
dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberParams)
@@ -289,14 +295,14 @@ private void TryPortMissingParamsForAPI(IDocsAPI dApiToUpdate, TripleSlashMember
{
foreach (DocsParam dParam in dApiToUpdate.Params)
{
- if (IsEmpty(dParam.Value))
+ if (dParam.Value.IsDocsEmpty())
{
created = false;
isEII = false;
name = string.Empty;
value = string.Empty;
- TripleSlashParam? tsParam = tsMemberToPort.Params.FirstOrDefault(x => x.Name == dParam.Name);
+ 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)
@@ -306,21 +312,21 @@ private void TryPortMissingParamsForAPI(IDocsAPI dApiToUpdate, TripleSlashMember
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}");
+ Log.Warning($" There were no IntelliSense xml comments for param '{dParam.Name}' in {dApiToUpdate.DocId}");
}
else
{
- created = TryPromptParam(dParam, tsMemberToPort, out TripleSlashParam? newTsParam);
+ created = TryPromptParam(dParam, tsMemberToPort, out IntelliSenseXmlParam? newTsParam);
if (newTsParam == null)
{
- Log.Error($" There param '{dParam.Name}' was not found in triple slash for {dApiToUpdate.DocId}");
+ Log.Error($" The param '{dParam.Name}' was not found in IntelliSense xml for {dApiToUpdate.DocId}.");
}
else
{
// Now attempt to document it
- if (!IsEmpty(newTsParam.Value))
+ if (!newTsParam.Value.IsDocsEmpty())
{
- // try to port triple slash comments
+ // try to port IntelliSense xml comments
dParam.Value = newTsParam.Value;
name = newTsParam.Name;
value = newTsParam.Value;
@@ -341,9 +347,9 @@ private void TryPortMissingParamsForAPI(IDocsAPI dApiToUpdate, TripleSlashMember
}
}
// Attempt to port
- else if (!IsEmpty(tsParam.Value))
+ else if (!tsParam.Value.IsDocsEmpty())
{
- // try to port triple slash comments
+ // try to port IntelliSense xml comments
dParam.Value = tsParam.Value;
name = tsParam.Name;
value = tsParam.Value;
@@ -362,7 +368,7 @@ private void TryPortMissingParamsForAPI(IDocsAPI dApiToUpdate, TripleSlashMember
}
- if (!IsEmpty(value))
+ if (!value.IsDocsEmpty())
{
string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} ({GetIsCreated(created)}) param {name.Escaped()} = {value.Escaped()}";
PrintModifiedMember(message, dApiToUpdate.FilePath, dApiToUpdate.DocId);
@@ -375,10 +381,10 @@ private void TryPortMissingParamsForAPI(IDocsAPI dApiToUpdate, TripleSlashMember
{
foreach (DocsParam dParam in dApiToUpdate.Params)
{
- if (IsEmpty(dParam.Value))
+ if (dParam.Value.IsDocsEmpty())
{
DocsParam? interfacedParam = interfacedMember.Params.FirstOrDefault(x => x.Name == dParam.Name);
- if (interfacedParam != null && !IsEmpty(interfacedParam.Value))
+ if (interfacedParam != null && !interfacedParam.Value.IsDocsEmpty())
{
dParam.Value = interfacedParam.Value;
@@ -392,7 +398,7 @@ private void TryPortMissingParamsForAPI(IDocsAPI dApiToUpdate, TripleSlashMember
}
// 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)
+ private void TryPortMissingTypeParamsForAPI(IDocsAPI dApiToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember)
{
if (dApiToUpdate.Kind == APIKind.Type && !Config.PortTypeTypeParams ||
dApiToUpdate.Kind == APIKind.Member && !Config.PortMemberTypeParams)
@@ -402,7 +408,7 @@ private void TryPortMissingTypeParamsForAPI(IDocsAPI dApiToUpdate, TripleSlashMe
if (tsMemberToPort != null)
{
- foreach (TripleSlashTypeParam tsTypeParam in tsMemberToPort.TypeParams)
+ foreach (IntelliSenseXmlTypeParam tsTypeParam in tsMemberToPort.TypeParams)
{
bool isEII = false;
string name = string.Empty;
@@ -419,10 +425,10 @@ private void TryPortMissingTypeParamsForAPI(IDocsAPI dApiToUpdate, TripleSlashMe
}
// But it can still be empty, try to retrieve it
- if (IsEmpty(dTypeParam.Value))
+ if (dTypeParam.Value.IsDocsEmpty())
{
- // try to port triple slash comments
- if (!IsEmpty(tsTypeParam.Value))
+ // try to port IntelliSense xml comments
+ if (!tsTypeParam.Value.IsDocsEmpty())
{
name = tsTypeParam.Name;
value = tsTypeParam.Value;
@@ -440,7 +446,7 @@ private void TryPortMissingTypeParamsForAPI(IDocsAPI dApiToUpdate, TripleSlashMe
}
}
- if (!IsEmpty(value))
+ if (!value.IsDocsEmpty())
{
dTypeParam.Value = value;
string message = $"{dApiToUpdate.Kind} {GetIsEII(isEII)} ({GetIsCreated(created)}) typeparam {name.Escaped()} = {value.Escaped()}";
@@ -452,14 +458,14 @@ private void TryPortMissingTypeParamsForAPI(IDocsAPI dApiToUpdate, TripleSlashMe
}
// Tries to document the passed property.
- private void TryPortMissingPropertyForMember(DocsMember dMemberToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember)
+ private void TryPortMissingPropertyForMember(DocsMember dMemberToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember)
{
if (!Config.PortMemberProperties)
{
return;
}
- if (IsEmpty(dMemberToUpdate.Value))
+ if (dMemberToUpdate.Value.IsDocsEmpty())
{
string name = string.Empty;
string value = string.Empty;
@@ -469,11 +475,11 @@ private void TryPortMissingPropertyForMember(DocsMember dMemberToUpdate, TripleS
if (tsMemberToPort != null)
{
name = tsMemberToPort.Name;
- if (!IsEmpty(tsMemberToPort.Value))
+ if (!tsMemberToPort.Value.IsDocsEmpty())
{
value = tsMemberToPort.Value;
}
- else if (!IsEmpty(tsMemberToPort.Returns))
+ else if (!tsMemberToPort.Returns.IsDocsEmpty())
{
value = tsMemberToPort.Returns;
}
@@ -482,11 +488,11 @@ private void TryPortMissingPropertyForMember(DocsMember dMemberToUpdate, TripleS
else if (interfacedMember != null)
{
name = interfacedMember.MemberName;
- if (!IsEmpty(interfacedMember.Value))
+ if (!interfacedMember.Value.IsDocsEmpty())
{
value = interfacedMember.Value;
}
- else if (!IsEmpty(interfacedMember.Returns))
+ else if (!interfacedMember.Returns.IsDocsEmpty())
{
value = interfacedMember.Returns;
}
@@ -496,7 +502,7 @@ private void TryPortMissingPropertyForMember(DocsMember dMemberToUpdate, TripleS
}
}
- if (!IsEmpty(value))
+ if (!value.IsDocsEmpty())
{
dMemberToUpdate.Value = value;
string message = $"Member {GetIsEII(isEII)} property {name.Escaped()} = {value.Escaped()}";
@@ -507,14 +513,14 @@ private void TryPortMissingPropertyForMember(DocsMember dMemberToUpdate, TripleS
}
// Tries to document the passed method.
- private void TryPortMissingMethodForMember(DocsMember dMemberToUpdate, TripleSlashMember? tsMemberToPort, DocsMember? interfacedMember)
+ private void TryPortMissingMethodForMember(DocsMember dMemberToUpdate, IntelliSenseXmlMember? tsMemberToPort, DocsMember? interfacedMember)
{
if (!Config.PortMemberReturns)
{
return;
}
- if (IsEmpty(dMemberToUpdate.Returns))
+ if (dMemberToUpdate.Returns.IsDocsEmpty())
{
string name = string.Empty;
string value = string.Empty;
@@ -525,19 +531,19 @@ private void TryPortMissingMethodForMember(DocsMember dMemberToUpdate, TripleSla
{
ProblematicAPIs.AddIfNotExists($"Unexpected System.Void return value in Method=[{dMemberToUpdate.DocId}]");
}
- else if (tsMemberToPort != null && !IsEmpty(tsMemberToPort.Returns))
+ else if (tsMemberToPort != null && !tsMemberToPort.Returns.IsDocsEmpty())
{
name = tsMemberToPort.Name;
value = tsMemberToPort.Returns;
}
- else if (interfacedMember != null && !IsEmpty(interfacedMember.Returns))
+ else if (interfacedMember != null && !interfacedMember.Returns.IsDocsEmpty())
{
name = interfacedMember.MemberName;
value = interfacedMember.Returns;
isEII = true;
}
- if (!IsEmpty(value))
+ if (!value.IsDocsEmpty())
{
dMemberToUpdate.Returns = value;
string message = $"Method {GetIsEII(isEII)} returns {name.Escaped()} = {value.Escaped()}";
@@ -550,7 +556,7 @@ private void TryPortMissingMethodForMember(DocsMember dMemberToUpdate, TripleSla
// 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)
+ private void TryPortMissingExceptionsForMember(DocsMember dMemberToUpdate, IntelliSenseXmlMember? tsMemberToPort)
{
if (!Config.PortExceptionsExisting && !Config.PortExceptionsNew)
{
@@ -560,7 +566,7 @@ private void TryPortMissingExceptionsForMember(DocsMember dMemberToUpdate, Tripl
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)
+ foreach (IntelliSenseXmlException tsException in tsMemberToPort.Exceptions)
{
DocsException? dException = dMemberToUpdate.Exceptions.FirstOrDefault(x => x.Cref == tsException.Cref);
bool created = false;
@@ -588,7 +594,7 @@ private void TryPortMissingExceptionsForMember(DocsMember dMemberToUpdate, Tripl
if (dException != null)
{
- if (created || (!IsEmpty(tsException.Value) && IsEmpty(dException.Value)))
+ 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);
@@ -600,8 +606,8 @@ private void TryPortMissingExceptionsForMember(DocsMember dMemberToUpdate, Tripl
}
}
- // 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)
+ // 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;
@@ -618,7 +624,7 @@ private bool TryPromptParam(DocsParam oldDParam, TripleSlashMember tsMember, out
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(" 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]: ");
@@ -644,10 +650,10 @@ private bool TryPromptParam(DocsParam oldDParam, TripleSlashMember tsMember, out
int paramSelection = -1;
while (paramSelection == -1)
{
- Log.Info($"Triple slash params found in member '{tsMember.Name}':");
+ Log.Info($"IntelliSense xml params found in member '{tsMember.Name}':");
Log.Warning(" 0 - Exit program.");
int paramCounter = 1;
- foreach (TripleSlashParam param in tsMember.Params)
+ foreach (IntelliSenseXmlParam param in tsMember.Params)
{
Log.Info($" {paramCounter} - {param.Name}");
paramCounter++;
@@ -704,10 +710,7 @@ private bool TryPromptParam(DocsParam oldDParam, TripleSlashMember tsMember, out
///
/// 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.
+ /// The API unique identifier.
private void PrintModifiedMember(string message, string docsFilePath, string docId)
{
Log.Warning($" File: {docsFilePath}");
@@ -730,7 +733,7 @@ private void PrintUndocumentedAPIs()
Log.Line();
- static void TryPrintType(ref bool undocAPI, string typeDocId)
+ void TryPrintType(ref bool undocAPI, string typeDocId)
{
if (!undocAPI)
{
@@ -739,7 +742,7 @@ static void TryPrintType(ref bool undocAPI, string typeDocId)
}
};
- static void TryPrintMember(ref bool undocMember, string memberDocId)
+ void TryPrintMember(ref bool undocMember, string memberDocId)
{
if (!undocMember)
{
@@ -757,10 +760,11 @@ static void TryPrintMember(ref bool undocMember, string memberDocId)
int exceptions = 0;
Log.Info("Undocumented APIs:");
+
foreach (DocsType docsType in DocsComments.Types)
{
bool undocAPI = false;
- if (IsEmpty(docsType.Summary))
+ if (docsType.Summary.IsDocsEmpty())
{
TryPrintType(ref undocAPI, docsType.DocId);
Log.Error($" Type Summary: {docsType.Summary}");
@@ -772,7 +776,7 @@ static void TryPrintMember(ref bool undocMember, string memberDocId)
{
bool undocMember = false;
- if (IsEmpty(member.Summary))
+ if (member.Summary.IsDocsEmpty())
{
TryPrintMember(ref undocMember, member.DocId);
@@ -803,7 +807,7 @@ static void TryPrintMember(ref bool undocMember, string memberDocId)
foreach (DocsParam param in member.Params)
{
- if (IsEmpty(param.Value))
+ if (param.Value.IsDocsEmpty())
{
TryPrintMember(ref undocMember, member.DocId);
@@ -814,7 +818,7 @@ static void TryPrintMember(ref bool undocMember, string memberDocId)
foreach (DocsTypeParam typeParam in member.TypeParams)
{
- if (IsEmpty(typeParam.Value))
+ if (typeParam.Value.IsDocsEmpty())
{
TryPrintMember(ref undocMember, member.DocId);
@@ -825,7 +829,7 @@ static void TryPrintMember(ref bool undocMember, string memberDocId)
foreach (DocsException exception in member.Exceptions)
{
- if (IsEmpty(exception.Value))
+ if (exception.Value.IsDocsEmpty())
{
TryPrintMember(ref undocMember, member.DocId);
diff --git a/Libraries/ToTripleSlashPorter.cs b/Libraries/ToTripleSlashPorter.cs
new file mode 100644
index 0000000..526275a
--- /dev/null
+++ b/Libraries/ToTripleSlashPorter.cs
@@ -0,0 +1,349 @@
+#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.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Loader;
+
+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;
+
+#pragma warning disable RS1024 // Compare symbols correctly
+ // Bug fixed https://github.com/dotnet/roslyn-analyzers/pull/4571
+ private readonly 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;
+ }
+ }
+
+ private ToTripleSlashPorter(Configuration config)
+ {
+ if (config.Direction != Configuration.PortingDirection.ToTripleSlash)
+ {
+ throw new InvalidOperationException($"Unexpected porting direction: {config.Direction}");
+ }
+
+ 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();
+
+ var porter = new ToTripleSlashPorter(config);
+ porter.Port();
+ }
+
+ private void Port()
+ {
+ DocsComments.CollectFiles();
+ if (!DocsComments.Types.Any())
+ {
+ Log.Error("No Docs Type APIs found.");
+ }
+
+ Log.Info("Porting from Docs to triple slash...");
+
+ // Load and store the main project
+ ProjectData mainProjectData = GetProjectData(Config.CsProj!.FullName);
+
+ foreach (DocsType docsType in DocsComments.Types)
+ {
+ // 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)
+ {
+ 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}.");
+
+ 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)
+ {
+ // The symbol has to live in one of the current project's referenced projects
+ foreach (ProjectReference projectReference in mainProjectData.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 extraProjectData = GetProjectDataAndSymbol(projectPath, docsType.FullName, out INamedTypeSymbol? actualSymbol);
+
+ ResolvedSymbols.Add(actualSymbol, new SymbolData { Api = docsType, ProjectData = extraProjectData });
+ }
+ }
+ else
+ {
+ ResolvedSymbols.Add(symbol, new SymbolData { Api = docsType, ProjectData = mainProjectData });
+ }
+ }
+
+
+ foreach ((ISymbol symbol, SymbolData data) in ResolvedSymbols)
+ {
+ ProjectData t = data.ProjectData;
+ foreach (Location location in symbol.Locations)
+ {
+ SyntaxTree tree = location.SourceTree
+ ?? throw new NullReferenceException($"Tree null for {data.Api.FullName}");
+
+ SemanticModel model = t.Compilation.GetSemanticModel(tree);
+ 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 static void CheckDiagnostics(MSBuildWorkspace workspace, string stepName)
+ {
+ ImmutableList diagnostics = workspace.Diagnostics;
+ if (diagnostics.Any())
+ {
+ 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);
+
+ if (!msg.Contains("Warning - Found project reference without a matching metadata reference"))
+ {
+ allMsgs.Add(msg);
+ }
+ }
+
+ if (allMsgs.Count > 1)
+ {
+ throw new Exception("Exiting due to diagnostic errors found: " + Environment.NewLine + string.Join(Environment.NewLine, allMsgs));
+ }
+ }
+ }
+
+ 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 pd = new();
+
+ try
+ {
+ pd.Workspace = MSBuildWorkspace.Create();
+ }
+ catch (ReflectionTypeLoadException)
+ {
+ Log.Error("The MSBuild directory was not found in PATH. Use '-MSBuild ' to specify it.");
+ throw;
+ }
+
+ CheckDiagnostics(pd.Workspace, "MSBuildWorkspace.Create");
+
+ pd.Project = pd.Workspace.OpenProjectAsync(csprojPath, msbuildLogger: BinLogger).Result
+ ?? throw new NullReferenceException($"Could not find the project: {csprojPath}");
+
+ CheckDiagnostics(pd.Workspace, $"workspace.OpenProjectAsync - {csprojPath}");
+
+ pd.Compilation = pd.Project.GetCompilationAsync().Result
+ ?? throw new NullReferenceException("The project's compilation was null.");
+
+ CheckDiagnostics(pd.Workspace, $"project.GetCompilationAsync - {csprojPath}");
+
+ return pd;
+ }
+
+ #region MSBuild loading logic
+
+ private static readonly Dictionary s_pathsToAssemblies = new(StringComparer.OrdinalIgnoreCase);
+ private static readonly Dictionary s_namesToAssemblies = new();
+
+ private static readonly object s_guard = new();
+
+ // 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) =>
+ {
+ 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/DocsPortingTool/XmlHelper.cs b/Libraries/XmlHelper.cs
similarity index 85%
rename from DocsPortingTool/XmlHelper.cs
rename to Libraries/XmlHelper.cs
index 4b6effc..7e0b1d6 100644
--- a/DocsPortingTool/XmlHelper.cs
+++ b/Libraries/XmlHelper.cs
@@ -1,12 +1,13 @@
-using System;
+#nullable enable
+using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
-namespace DocsPortingTool
+namespace Libraries
{
- public class XmlHelper
+ internal class XmlHelper
{
private static readonly Dictionary _replaceableNormalElementPatterns = new Dictionary {
{ "null", ""},
@@ -86,12 +87,11 @@ 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
{
- 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)
{
@@ -128,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();
}
@@ -138,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
@@ -147,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;
@@ -171,14 +168,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);
@@ -189,8 +184,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;
@@ -221,8 +215,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);
@@ -232,14 +225,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/Program/DocsPortingTool.cs b/Program/DocsPortingTool.cs
new file mode 100644
index 0000000..828b39b
--- /dev/null
+++ b/Program/DocsPortingTool.cs
@@ -0,0 +1,30 @@
+#nullable enable
+using Libraries;
+using System;
+
+namespace DocsPortingTool
+{
+ class DocsPortingTool
+ {
+ public static void Main(string[] args)
+ {
+ Configuration config = Configuration.GetCLIArgumentsForDocsPortingTool(args);
+ switch (config.Direction)
+ {
+ case Configuration.PortingDirection.ToDocs:
+ {
+ ToDocsPorter porter = new(config);
+ porter.Start();
+ break;
+ }
+ case Configuration.PortingDirection.ToTripleSlash:
+ {
+ ToTripleSlashPorter.Start(config);
+ break;
+ }
+ default:
+ throw new ArgumentOutOfRangeException($"Unrecognized porting direction: {config.Direction}");
+ }
+ }
+ }
+}
diff --git a/Program/DocsPortingTool.csproj b/Program/DocsPortingTool.csproj
new file mode 100644
index 0000000..47e19b5
--- /dev/null
+++ b/Program/DocsPortingTool.csproj
@@ -0,0 +1,27 @@
+
+
+
+ Exe
+ net5.0
+ DocsPortingTool.DocsPortingTool
+ Microsoft
+ carlossanlop
+ enable
+ true
+ true
+ 3.0.0
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Program/Properties/launchSettings.json b/Program/Properties/launchSettings.json
new file mode 100644
index 0000000..80b814e
--- /dev/null
+++ b/Program/Properties/launchSettings.json
@@ -0,0 +1,17 @@
+{
+ "profiles": {
+ "Program": {
+ "commandName": "Project",
+ "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\\"
+ }
+ }
+ }
+}
\ No newline at end of file
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/PortToDocsTestData.cs b/Tests/PortToDocs/PortToDocsTestData.cs
new file mode 100644
index 0000000..8b7bb09
--- /dev/null
+++ b/Tests/PortToDocs/PortToDocsTestData.cs
@@ -0,0 +1,82 @@
+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 string DocsOriginFilePath { 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));
+
+ 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);
+
+ 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), "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);
+ Assert.True(File.Exists(ActualFilePath), "Verify docs original file (copied) exists.");
+
+ 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), "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), "Verify docs interface file (copied) exists.");
+ }
+ }
+ }
+
+}
diff --git a/Tests/Tests.cs b/Tests/PortToDocs/PortToDocsTests.cs
similarity index 58%
rename from Tests/Tests.cs
rename to Tests/PortToDocs/PortToDocsTests.cs
index 92a3231..9b47f41 100644
--- a/Tests/Tests.cs
+++ b/Tests/PortToDocs/PortToDocsTests.cs
@@ -1,69 +1,74 @@
-using System.Collections.Generic;
+using System;
using System.IO;
using Xunit;
+using Xunit.Abstractions;
-namespace DocsPortingTool.Tests
+namespace Libraries.Tests
{
- public class Tests
+ public class PortToDocsTests : BasePortTests
{
+ public PortToDocsTests(ITestOutputHelper output) : base(output)
+ {
+ }
+
[Fact]
// 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 +77,7 @@ public void Port_Remarks_WithEII_NoInterfaceRemarks()
/// Verifies that new exceptions are ported.
public void Port_Exceptions()
{
- Port("Exceptions");
+ PortToDocs("Exceptions");
}
[Fact]
@@ -80,12 +85,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,
@@ -102,7 +107,7 @@ private void Port(
{
using TestDirectory tempDir = new TestDirectory();
- TestData testData = new TestData(
+ PortToDocsTestData testData = new PortToDocsTestData(
tempDir,
testDataDir,
skipInterfaceImplementations: skipInterfaceImplementations,
@@ -111,17 +116,18 @@ private void Port(
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);
@@ -131,29 +137,69 @@ private void Port(
c.IncludedNamespaces.Add(namespaceName);
}
- c.DirsDocsXml.Add(testData.Docs);
- c.DirsTripleSlashXmls.Add(testData.TripleSlash);
+ c.DirsDocsXml.Add(testData.DocsDir);
+ c.DirsIntelliSense.Add(testData.IntelliSenseAndDLLDir);
- Analyzer analyzer = new Analyzer(c);
- analyzer.Start();
+ var porter = new ToDocsPorter(c);
+ porter.Start();
Verify(testData);
}
- private void Verify(TestData testData)
+ private void Verify(PortToDocsTestData testData)
{
string[] expectedLines = File.ReadAllLines(testData.ExpectedFilePath);
string[] actualLines = File.ReadAllLines(testData.ActualFilePath);
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)
+ {
+ string expected = GetProblematicLines("Expected", expectedLines, i);
+ string actual = GetProblematicLines("Actual", actualLines, i);
+
+ Output.WriteLine(expected);
+ Output.WriteLine(actual);
+ }
+
Assert.Equal(expectedLine, actualLine);
}
+ // Check at the end, because we first want to fail on different lines
Assert.Equal(expectedLines.Length, actualLines.Length);
}
+
+ private static string GetProblematicLines(string title, string[] lines, int lineNumber)
+ {
+ string output = $"{title}:{Environment.NewLine}";
+
+ for (int i = 5; i >= 1; i--)
+ {
+ if ((lineNumber - i) >= 0)
+ {
+ output += $"[{lineNumber - i}] {lines[lineNumber - i]}{Environment.NewLine}";
+ }
+ }
+
+ output += $"[{lineNumber}] {lines[lineNumber]}{Environment.NewLine}";
+
+ for (int i = 1; i <= 5; i++)
+ {
+ if ((lineNumber + i) < lines.Length)
+ {
+ output += $"[{lineNumber + i}] {lines[lineNumber + i]}{Environment.NewLine}";
+ }
+ }
+
+ return output;
+ }
}
}
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 96%
rename from Tests/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml
rename to Tests/PortToDocs/TestData/Remarks_WithEII_WithInterfaceRemarks/DocsExpected.xml
index 33bdbee..c41cae6 100644
--- a/Tests/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`.
]]>
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
diff --git a/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs b/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs
new file mode 100644
index 0000000..9357b2b
--- /dev/null
+++ b/Tests/PortToTripleSlash/PortToTripleSlashTestData.cs
@@ -0,0 +1,52 @@
+using System.IO;
+using Xunit;
+
+namespace Libraries.Tests
+{
+ internal class PortToTripleSlashTestData : TestData
+ {
+ private string TestDataRootDirPath => @"../../../PortToTripleSlash/TestData";
+ private const string ProjectDirName = "Project";
+ 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));
+
+ namespaceName = string.IsNullOrEmpty(namespaceName) ? assemblyName : namespaceName;
+
+ ProjectDir = tempDir.CreateSubdirectory(ProjectDirName);
+
+ DocsDir = tempDir.CreateSubdirectory(DocsDirName);
+ DirectoryInfo docsAssemblyDir = DocsDir.CreateSubdirectory(namespaceName);
+
+ string testDataPath = Path.Combine(TestDataRootDirPath, testDataDir);
+
+ 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/PortToTripleSlashTests.cs b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs
new file mode 100644
index 0000000..7c62cbc
--- /dev/null
+++ b/Tests/PortToTripleSlash/PortToTripleSlashTests.cs
@@ -0,0 +1,81 @@
+#nullable enable
+using System.IO;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Libraries.Tests
+{
+ public class PortToTripleSlashTests : BasePortTests
+ {
+ public PortToTripleSlashTests(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [Fact]
+ public void Port_Basic()
+ {
+ PortToTripleSlash("Basic");
+ }
+
+ private static void PortToTripleSlash(
+ string testDataDir,
+ bool save = true,
+ bool skipInterfaceImplementations = true,
+ string assemblyName = TestData.TestAssembly,
+ string namespaceName = TestData.TestNamespace,
+ string typeName = TestData.TestType)
+ {
+ using TestDirectory tempDir = new();
+
+ PortToTripleSlashTestData testData = new(
+ tempDir,
+ testDataDir,
+ assemblyName: assemblyName,
+ namespaceName: namespaceName,
+ typeName: typeName);
+
+ Configuration c = new()
+ {
+ Direction = Configuration.PortingDirection.ToTripleSlash,
+ CsProj = new FileInfo(testData.ProjectFilePath),
+ Save = save,
+ SkipInterfaceImplementations = skipInterfaceImplementations
+ };
+
+ c.IncludedAssemblies.Add(assemblyName);
+
+ if (!string.IsNullOrEmpty(namespaceName))
+ {
+ c.IncludedNamespaces.Add(namespaceName);
+ }
+
+ c.DirsDocsXml.Add(testData.DocsDir);
+
+ ToTripleSlashPorter.Start(c);
+
+ Verify(testData);
+ }
+
+ private static 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];
+ if (System.Diagnostics.Debugger.IsAttached)
+ {
+ if (expectedLine != actualLine)
+ {
+ System.Diagnostics.Debugger.Break();
+ }
+ }
+ Assert.Equal(expectedLine, actualLine);
+ }
+
+ Assert.Equal(expectedLines.Length, actualLines.Length);
+ }
+ }
+}
diff --git a/Tests/PortToTripleSlash/TestData/Basic/MyAssembly.csproj b/Tests/PortToTripleSlash/TestData/Basic/MyAssembly.csproj
new file mode 100644
index 0000000..4d7f14e
--- /dev/null
+++ b/Tests/PortToTripleSlash/TestData/Basic/MyAssembly.csproj
@@ -0,0 +1,9 @@
+
+
+
+ Library
+ This is MyNamespace description.
+ net5.0
+
+
+
diff --git a/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml b/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml
new file mode 100644
index 0000000..2f13585
--- /dev/null
+++ b/Tests/PortToTripleSlash/TestData/Basic/MyDelegate.xml
@@ -0,0 +1,16 @@
+
+
+
+ MyAssembly
+
+
+ This is the sender parameter.
+ This is the e parameter.
+ This is the MyDelegate typeparam T.
+ 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/MyType.xml b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml
new file mode 100644
index 0000000..ec42063
--- /dev/null
+++ b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml
@@ -0,0 +1,206 @@
+
+
+
+ MyAssembly
+
+
+ This is the MyType class summary.
+
+
+
+
+
+
+
+ Constructor
+
+ MyAssembly
+
+
+ This is the MyType constructor summary.
+ To be added.
+
+
+
+
+ Property
+
+ MyAssembly
+
+
+ This is the MyProperty summary.
+ This is the MyProperty value.
+
+ and the xref uses displayProperty, which should be ignored when porting.
+
+ ]]>
+
+
+
+
+
+ Field
+
+ MyAssembly
+
+ 1
+
+ This is the MyField summary.
+
+There is a primitive type here.
+
+ here.
+
+Multiple lines.
+
+ ]]>
+
+
+
+
+
+ Method
+
+ MyAssembly
+
+
+ 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 .
+
+ and the `param2`.
+
+There are also a `true` and a `null`.
+
+ ]]>
+
+ This is the ArgumentNullException thrown by MyIntMethod. It mentions the .
+ This is the IndexOutOfRangeException thrown by MyIntMethod.
+
+
+
+
+ Method
+
+ MyAssembly
+
+
+ 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 .
+ 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
+
+
+ This is the MyTypeParamMethod typeparam T.
+ This is the MyTypeParamMethod parameter param1.
+ This is the MyTypeParamMethod summary.
+
+
+
+
+
+
+
+ Event
+
+ MyAssembly
+
+
+ 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.
+
+
+
+
\ No newline at end of file
diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs
new file mode 100644
index 0000000..cabbcd2
--- /dev/null
+++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs
@@ -0,0 +1,119 @@
+using System;
+
+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 and the xref uses displayProperty, which should be ignored when porting.
+ public int MyProperty
+ {
+ get { return _myProperty; /* Internal comments should remain untouched. */ }
+ set { _myProperty = value; } // Internal comments should remain untouched
+ }
+
+ /// This is the MyField summary.
+ /// There is a primitive type here.
+ /// These are the MyField remarks.
+ /// There is a primitive type here.
+ /// 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 , 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)
+ {
+ // Internal comments should remain untouched.
+ return MyField + param1 + param2;
+ }
+
+ /// This is the MyVoidMethod summary.
+ /// These are the MyVoidMethod remarks.
+ /// Multiple lines.
+ /// 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-
+ /// 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.
+ /// 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, 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
new file mode 100644
index 0000000..cb1a1c9
--- /dev/null
+++ b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs
@@ -0,0 +1,75 @@
+using System;
+
+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; /* 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;
+ }
+
+ 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()
+ {
+ }
+
+ 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;
+ }
+ }
+}
diff --git a/Tests/TestData.cs b/Tests/TestData.cs
index 51157d8..ca56dd5 100644
--- a/Tests/TestData.cs
+++ b/Tests/TestData.cs
@@ -1,80 +1,17 @@
using System.IO;
-using Xunit;
-namespace DocsPortingTool.Tests
+namespace Libraries.Tests
{
- public class TestData
+ internal class TestData
{
- private string TestDataRootDir => @"..\..\..\TestData";
+ internal const string TestAssembly = "MyAssembly";
+ internal const string TestNamespace = "MyNamespace";
+ internal const string TestType = "MyType";
+ internal const string DocsDirName = "Docs";
- public const string TestAssembly = "MyAssembly";
- public const string TestNamespace = "MyNamespace";
- public const string TestType = "MyType";
+ internal string ExpectedFilePath { get; set; }
+ internal string ActualFilePath { get; set; }
+ internal DirectoryInfo DocsDir { get; set; }
- 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 Docs { get; private set; }
-
- /// Triple slash 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;
-
- TripleSlash = tempDir.CreateSubdirectory("TripleSlash");
- DirectoryInfo tsAssemblyDir = TripleSlash.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/TestDirectory.cs b/Tests/TestDirectory.cs
index 75a1961..0e47f70 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
{
@@ -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)
diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj
index 829934b..fdf6811 100644
--- a/Tests/Tests.csproj
+++ b/Tests/Tests.csproj
@@ -2,20 +2,39 @@
net5.0
-
+ Microsoft
+ carlossanlop
false
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
-
+
+
+
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