Skip to content

Commit bd55b04

Browse files
committed
Add support for generating LuaCATS definitions for the Lua API
1 parent 54b1b2e commit bd55b04

File tree

4 files changed

+189
-1
lines changed

4 files changed

+189
-1
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using System.Text;
6+
using System.Text.RegularExpressions;
7+
using BizHawk.Common;
8+
using NLua;
9+
10+
namespace BizHawk.Client.Common;
11+
12+
/// <summary>
13+
/// Generates API definitions in the LuaCATS format.
14+
/// </summary>
15+
/// <remarks>
16+
/// See https://luals.github.io/wiki/annotations
17+
/// </remarks>
18+
internal class LuaCatsGenerator
19+
{
20+
private static readonly Dictionary<Type, string> TypeConversions = new()
21+
{
22+
[typeof(object)] = "any",
23+
[typeof(int)] = "integer",
24+
[typeof(uint)] = "integer",
25+
[typeof(short)] = "integer",
26+
[typeof(ushort)] = "integer",
27+
[typeof(long)] = "integer",
28+
[typeof(ulong)] = "integer",
29+
[typeof(float)] = "number",
30+
[typeof(double)] = "number",
31+
[typeof(string)] = "string",
32+
[typeof(bool)] = "boolean",
33+
[typeof(LuaFunction)] = "function",
34+
[typeof(LuaTable)] = "table",
35+
[typeof(System.Drawing.Color)] = "color",
36+
};
37+
38+
private const string Preamble = @"---@meta _
39+
40+
---@class color : userdata
41+
42+
---A color in one of the following formats:
43+
--- - Number in the format `0xAARRGGBB`
44+
--- - String in the format `""#RRGGBB""` or `""#AARRGGBB""`
45+
--- - A CSS3/X11 color name e.g. `""blue""`, `""palegoldenrod""`
46+
--- - Color created with `forms.createcolor`
47+
---@alias luacolor integer | string | color
48+
";
49+
50+
public string Generate(LuaDocumentation docs)
51+
{
52+
var sb = new StringBuilder();
53+
54+
sb.AppendLine($"--Generated with BizHawk {VersionInfo.MainVersion}");
55+
56+
sb.AppendLine(Preamble);
57+
58+
foreach (var libraryGroup in docs.GroupBy(func => func.Library).OrderBy(group => group.Key))
59+
{
60+
string library = libraryGroup.Key;
61+
string libraryDescription = libraryGroup.First().LibraryDescription;
62+
63+
if (!string.IsNullOrEmpty(libraryDescription))
64+
sb.AppendLine(FormatDescription(libraryDescription));
65+
sb.AppendLine($"---@class {library}");
66+
sb.AppendLine($"{library} = {{}}");
67+
sb.AppendLine();
68+
69+
foreach (var func in libraryGroup.OrderBy(func => func.Name))
70+
{
71+
if (!string.IsNullOrEmpty(func.Description))
72+
sb.AppendLine(FormatDescription(func.Description));
73+
74+
if (func.IsDeprecated)
75+
sb.AppendLine("---@deprecated");
76+
77+
foreach (var parameter in func.Method.GetParameters())
78+
{
79+
if (IsParams(parameter))
80+
{
81+
sb.Append("---@vararg");
82+
}
83+
else
84+
{
85+
sb.Append($"---@param {parameter.Name}");
86+
if (parameter.IsOptional || IsNullable(parameter.ParameterType))
87+
sb.Append('?');
88+
}
89+
90+
sb.Append(' ');
91+
sb.AppendLine(GetLuaType(parameter));
92+
}
93+
94+
if (func.Method.ReturnType != typeof(void))
95+
{
96+
sb.Append("---@return ");
97+
sb.AppendLine(GetLuaType(func.Method.ReturnType));
98+
}
99+
100+
sb.Append($"function {library}.{func.Name}(");
101+
102+
foreach (var parameter in func.Method.GetParameters())
103+
{
104+
if (parameter.Position > 0)
105+
sb.Append(", ");
106+
sb.Append(IsParams(parameter) ? "..." : parameter.Name);
107+
}
108+
109+
sb.AppendLine(") end");
110+
sb.AppendLine();
111+
}
112+
sb.AppendLine();
113+
}
114+
return sb.ToString();
115+
}
116+
117+
private static string FormatDescription(string description)
118+
{
119+
// prefix every line with ---
120+
description = Regex.Replace(description, "^", "---", RegexOptions.Multiline);
121+
// replace {{wiki markup}} with `markdown`
122+
description = Regex.Replace(description, @"{{(.+?)}}", "`$1`");
123+
// replace wiki image markup with markdown
124+
description = Regex.Replace(description, @"\[(?<url>.+?)\|alt=(?<alt>.+?)\]", "![${alt}](${url})");
125+
return description;
126+
}
127+
128+
private static string GetLuaType(ParameterInfo parameter)
129+
{
130+
if (parameter.ParameterType == typeof(object) && parameter.GetCustomAttribute<LuaColorParamAttribute>() is not null)
131+
return "luacolor"; // see Preamble
132+
133+
// no [] array modifier for varargs
134+
if (parameter.ParameterType.IsArray && IsParams(parameter))
135+
return GetLuaType(parameter.ParameterType.GetElementType());
136+
137+
// technically any string parameter can be passed a number, but let's just focus on the ones where it's commonly used
138+
// like `gui.text` and `forms.settext` instead of polluting the entire API surface
139+
if (parameter.ParameterType == typeof(string) && parameter.Name is "message" or "caption")
140+
return "string | number";
141+
142+
return GetLuaType(parameter.ParameterType);
143+
}
144+
145+
private static string GetLuaType(Type type)
146+
{
147+
if (type.IsArray)
148+
return GetLuaType(type.GetElementType()) + "[]";
149+
150+
if (IsNullable(type))
151+
type = type.GetGenericArguments()[0];
152+
153+
if (TypeConversions.TryGetValue(type, out string luaType))
154+
return luaType;
155+
else
156+
throw new NotSupportedException($"Unknown type {type.FullName} used in API. Generator must be updated to handle this.");
157+
}
158+
159+
private static bool IsNullable(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
160+
161+
private static bool IsParams(ParameterInfo parameter) => parameter.GetCustomAttribute<ParamArrayAttribute>() is not null;
162+
}

src/BizHawk.Client.Common/lua/LuaDocumentation.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ public string ToNotepadPlusPlusAutoComplete()
191191
{
192192
return ""; // TODO
193193
}
194+
195+
public string ToLuaLanguageServerDefinitions()
196+
{
197+
var generator = new LuaCatsGenerator();
198+
return generator.Generate(this);
199+
}
194200
}
195201

196202
public class LibraryFunction

src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.Designer.cs

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,18 @@ private void RegisterNotePadMenuItem_Click(object sender, EventArgs e)
11861186
_luaAutoInstaller.InstallBizLua(LuaAutocompleteInstaller.TextEditors.NotePad, LuaImp.Docs);
11871187
}
11881188

1189+
private void GenerateLuaCatsDefinitionMenuItem_Click(object sender, EventArgs e)
1190+
{
1191+
string initDir = !string.IsNullOrWhiteSpace(LuaImp.ScriptList.Filename)
1192+
? Path.GetDirectoryName(LuaImp.ScriptList.Filename)
1193+
: Config!.PathEntries.LuaAbsolutePath();
1194+
1195+
if (this.ShowFileSaveDialog(initDir, initFileName: "BizHawk.lua", fileExt: ".lua", filter: JustScriptsFSFilterSet) is string path)
1196+
{
1197+
File.WriteAllText(path, LuaImp.Docs.ToLuaLanguageServerDefinitions());
1198+
}
1199+
}
1200+
11891201
private void FunctionsListMenuItem_Click(object sender, EventArgs e)
11901202
{
11911203
new LuaFunctionsForm(LuaImp.Docs).Show();

0 commit comments

Comments
 (0)