Skip to content
This repository has been archived by the owner on Oct 22, 2024. It is now read-only.

Commit

Permalink
Working on an API documentation generator
Browse files Browse the repository at this point in the history
  • Loading branch information
Malware committed Jul 22, 2018
1 parent 57f8011 commit bcde381
Show file tree
Hide file tree
Showing 7 changed files with 472 additions and 11 deletions.
5 changes: 5 additions & 0 deletions Source/DocGen/DocGen.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
Expand All @@ -43,9 +44,13 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="MDKUtilityFramework.cs" />
<Compile Include="ProgrammableBlockApi.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Terminals.cs" />
<Compile Include="Whitelist.cs" />
<Compile Include="WhitelistKey.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
Expand Down
55 changes: 55 additions & 0 deletions Source/DocGen/MDKUtilityFramework.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace DocGen
{
/// <summary>
/// Framework initialization and configuration
/// </summary>
public class MDKUtilityFramework
{
static readonly Dictionary<string, AssemblyName> AssemblyNames = new Dictionary<string, AssemblyName>();

/// <summary>
/// Gets the game binary path as defined through <see cref="Load"/>.
/// </summary>
public static string GameBinPath { get; internal set; }

/// <summary>
/// Initializes the mock system. Pass in the path to the Space Engineers Bin64 folder.
/// </summary>
/// <param name="mdkOptionsPath">The path to the MDK options file</param>
public static void Load(string gameBinPath)
{
var directory = new DirectoryInfo(gameBinPath);

foreach (var dllFileName in directory.EnumerateFiles("*.dll"))
{
AssemblyName assemblyName;
try
{
assemblyName = AssemblyName.GetAssemblyName(dllFileName.FullName);
}
catch (BadImageFormatException)
{
// Not a .NET assembly or wrong platform, ignore
continue;
}
AssemblyNames[assemblyName.FullName] = assemblyName;
//AssemblyNames[dllFileName.FullName.Replace("\\", "\\\\")] = assemblyName;
}
AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;

GameBinPath = directory.FullName;
}

static Assembly OnResolveAssembly(object sender, ResolveEventArgs args)
{
if (AssemblyNames.TryGetValue(args.Name, out AssemblyName assemblyName))
return Assembly.Load(assemblyName);
return null;
}
}
}
18 changes: 13 additions & 5 deletions Source/DocGen/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ namespace DocGen
{
public class Program
{
public static int Main()
public static async Task<int> Main()
{
var commandLine = new CommandLine(Environment.CommandLine);
try
{
var program = new Program();
return Task.Run(async () => await program.Run(commandLine).ConfigureAwait(false)).GetAwaiter().GetResult();
return await program.Run(commandLine);
}
catch (Exception e)
{
Expand All @@ -38,11 +38,13 @@ async Task UpdateCaches(string path)
var pluginPath = Path.GetFullPath("MDKWhitelistExtractor.dll");
var whitelistTarget = path;
var terminalTarget = path;
var apiTarget = path;
var directoryInfo = new DirectoryInfo(whitelistTarget);
if (!directoryInfo.Exists)
directoryInfo.Create();
whitelistTarget = Path.Combine(whitelistTarget, "whitelist.cache");
terminalTarget = Path.Combine(terminalTarget, "terminal.cache");
apiTarget = Path.Combine(apiTarget, "api.cache");

var args = new List<string>
{
Expand All @@ -52,7 +54,9 @@ async Task UpdateCaches(string path)
"-whitelistcaches",
$"\"{whitelistTarget}\"",
"-terminalcaches",
$"\"{terminalTarget}\""
$"\"{terminalTarget}\"",
"-pbapi",
$"\"{apiTarget}\""
};

var process = new Process
Expand Down Expand Up @@ -121,13 +125,17 @@ async Task<int> Run(CommandLine commandLine)
if (outputIndex >= 0)
output = Path.GetFullPath(commandLine[outputIndex + 1]);
if (output != null)
GenerateDocs(path, output);
await GenerateDocs(path, output);

return 0;
}

void GenerateDocs(string path, string output)
async Task GenerateDocs(string path, string output)
{
var api = new ProgrammableBlockApi();
await api.Scan(Path.Combine(path, "whitelist.cache"));
await api.SaveAsync(Path.Combine(output, "api"));

//var whitelistTarget = path;
var terminalTarget = path;
//whitelistTarget = Path.Combine(whitelistTarget, "whitelist.cache");
Expand Down
169 changes: 169 additions & 0 deletions Source/DocGen/ProgrammableBlockApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;
using System.Xml.XPath;
using Malware.MDKUtilities;

namespace DocGen
{
class ProgrammableBlockApi
{
List<MemberInfo> _members = new List<MemberInfo>();
List<Assembly> _assemblies;
List<IGrouping<Assembly, IGrouping<Type, MemberInfo>>> _groupings;

public async Task Scan(string whitelistCacheFileName)
{
await Task.Run(() =>
{
var whitelist = Whitelist.Load(whitelistCacheFileName);
var spaceEngineers = new SpaceEngineers();
var installPath = Path.Combine(spaceEngineers.GetInstallPath(), "bin64");
MDKUtilityFramework.Load(installPath);
var dllFiles = Directory.EnumerateFiles(installPath, "*.dll", SearchOption.TopDirectoryOnly)
.ToList();

foreach (var dllFile in dllFiles)
Visit(whitelist, dllFile);

_groupings = _members.GroupBy(m => m.DeclaringType).GroupBy(m => m.Key.Assembly)
.ToList();
});
}

void Visit(Whitelist whitelist, string dllFile)
{
try
{
var assemblyName = AssemblyName.GetAssemblyName(dllFile);
var assembly = Assembly.Load(assemblyName);
Visit(whitelist, assembly);
}
catch (FileLoadException e)
{ }
catch (BadImageFormatException e)
{ }
}

void Visit(Whitelist whitelist, Assembly assembly)
{
if (!whitelist.IsWhitelisted(assembly))
return;
if (assembly.GetName().Name == "mscorlib")
return;
var companyAttribute = assembly.GetCustomAttribute<AssemblyCompanyAttribute>();
if (companyAttribute?.Company == "Microsoft Corporation")
return;
var types = assembly.GetExportedTypes();
foreach (var type in types)
Visit(whitelist, type);
}

void Visit(Whitelist whitelist, Type type)
{
if (!type.IsPublic)
return;
var members = type.GetMembers(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
foreach (var member in members)
Visit(whitelist, member);
}

void Visit(Whitelist whitelist, MemberInfo member)
{
if (!whitelist.IsWhitelisted(member))
return;
_members.Add(member);
}

public async Task SaveAsync(string path)
{
var directory = new DirectoryInfo(Path.Combine(path, "api"));
if (!directory.Exists)
directory.Create();
var fileName = Path.Combine(directory.FullName, "index.md");
using (var file = File.CreateText(fileName))
{
await file.WriteLineAsync("#Index");
await file.WriteLineAsync();

foreach (var assemblyGroup in _groupings.OrderBy(g => g.Key.GetName().Name))
{
var assemblyPath = new Uri(assemblyGroup.Key.CodeBase).LocalPath;
var assemblyFileName = Path.GetFileName(assemblyPath);
var xmlFileName = Path.ChangeExtension(assemblyPath, "xml");
XDocument documentation;
if (File.Exists(xmlFileName))
documentation = XDocument.Load(xmlFileName);
else
documentation = null;

await file.WriteLineAsync($"##{assemblyFileName}");
foreach (var typeGroup in assemblyGroup.OrderBy(g => g.Key.FullName))
{
var typeKey = WhitelistKey.ForType(typeGroup.Key);
var mdPath = ToMdFileName(typeKey.Path);
await file.WriteLineAsync($"**[`{typeKey.Path}`]({mdPath})**");
foreach (var member in typeGroup.OrderBy(m => m.Name))
{
var memberKey = WhitelistKey.ForMember(member, false);
await file.WriteLineAsync($"* [`{memberKey.Path}`]({mdPath})");
}
await file.WriteLineAsync();
await WriteTypeFileAsync(typeGroup, Path.Combine(directory.FullName, mdPath), documentation);
}
}

file.Flush();
}
}

async Task WriteTypeFileAsync(IGrouping<Type, MemberInfo> typeGroup, string fileName, XDocument documentation)
{
using (var file = File.CreateText(fileName))
{
var typeKey = WhitelistKey.ForType(typeGroup.Key);
await file.WriteLineAsync($"#{typeKey.Path}");
await file.WriteLineAsync();
foreach (var member in typeGroup.OrderBy(m => m.Name))
{
var fullMemberKey = WhitelistKey.ForMember(member);
var xmlKey = fullMemberKey.ToXmlDoc();
var memberKey = WhitelistKey.ForMember(member, false);
var doc = documentation?.XPathSelectElement($"/doc/members/member[@name='{xmlKey}']");
string summary;
if (doc != null)
summary = doc.Element("summary")?.Value ?? "";
else
summary = "";
await file.WriteLineAsync($"* `{memberKey.Path}`");
await file.WriteLineAsync($" " + Trim(summary));
await file.WriteLineAsync();
}
}
}

string Trim(string summary) => Regex.Replace(summary.Trim(), @"\s{2,}", " ");

readonly HashSet<char> _invalidCharacters = new HashSet<char>(Path.GetInvalidFileNameChars());

string ToMdFileName(string path)
{
var builder = new StringBuilder(path);
for (var i = 0; i < builder.Length; i++)
{
if (_invalidCharacters.Contains(builder[i]))
builder[i] = '_';
}

builder.Append(".md");
return builder.ToString();
}
}
}
8 changes: 2 additions & 6 deletions Source/DocGen/Terminals.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.IO;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
Expand Down Expand Up @@ -30,6 +29,7 @@ void Load(XElement root)
return;
var blocks = root.Elements("block").OrderBy(block => GetBlockName((string)block.Attribute("type"))).ToArray();
_document.AppendLine($"## Overview");
_document.AppendLine("**Note: Terminal actions and properties are for all intents and purposes obsolete since all vanilla block interfaces now contain proper API access to all this information. It is highly recommended you use those for less overhead.**");
_document.AppendLine();
foreach (var block in blocks)
{
Expand All @@ -50,9 +50,7 @@ void Load(XElement root)
_document.AppendLine("|Name|Description|");
_document.AppendLine("|-|-|");
foreach (var action in elements)
{
_document.AppendLine($"|{(string)action.Attribute("name")}|{(string)action.Attribute("text")}|");
}
_document.AppendLine();
}
elements = block.Elements("property").OrderBy(e => (string)e.Attribute("name")).ToArray();
Expand All @@ -63,9 +61,7 @@ void Load(XElement root)
_document.AppendLine("|Name|Type|");
_document.AppendLine("|-|-|");
foreach (var action in elements)
{
_document.AppendLine($"|{(string)action.Attribute("name")}|{TranslateType((string)action.Attribute("type"))}|");
}
_document.AppendLine();
}
}
Expand Down
Loading

0 comments on commit bcde381

Please sign in to comment.