mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
feat: Add script line and column details to C# compile errors
feat: Add script line and column details to C# compile errors
This commit is contained in:
commit
35db711ea4
3 changed files with 147 additions and 42 deletions
|
|
@ -1780,6 +1780,51 @@ namespace MinecraftClient {
|
||||||
return ResourceManager.GetString("bot.script.pm.loaded", resourceCulture);
|
return ResourceManager.GetString("bot.script.pm.loaded", resourceCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Error in {0} at line {1}, column {2}: [{3}] {4}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string script_compile_error {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("script.compile.error", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Error in {0}: [{1}] {2}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string script_compile_error_no_location {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("script.compile.error_no_location", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to [Script] Compilation failed with error(s):.
|
||||||
|
/// </summary>
|
||||||
|
internal static string script_compile_failed {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("script.compile.failed", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to [Script] Starting compilation for {0}....
|
||||||
|
/// </summary>
|
||||||
|
internal static string script_compile_started {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("script.compile.started", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to [Script] Compilation done with no errors..
|
||||||
|
/// </summary>
|
||||||
|
internal static string script_compile_succeeded {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("script.compile.succeeded", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Loaded task:
|
/// Looks up a localized string similar to Loaded task:
|
||||||
|
|
|
||||||
|
|
@ -693,6 +693,21 @@ cooldown: {6}</value>
|
||||||
<data name="bot.script.pm.loaded" xml:space="preserve">
|
<data name="bot.script.pm.loaded" xml:space="preserve">
|
||||||
<value>Script '{0}' loaded.</value>
|
<value>Script '{0}' loaded.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="script.compile.error" xml:space="preserve">
|
||||||
|
<value>Error in {0} at line {1}, column {2}: [{3}] {4}</value>
|
||||||
|
</data>
|
||||||
|
<data name="script.compile.error_no_location" xml:space="preserve">
|
||||||
|
<value>Error in {0}: [{1}] {2}</value>
|
||||||
|
</data>
|
||||||
|
<data name="script.compile.failed" xml:space="preserve">
|
||||||
|
<value>[Script] Compilation failed with error(s):</value>
|
||||||
|
</data>
|
||||||
|
<data name="script.compile.started" xml:space="preserve">
|
||||||
|
<value>[Script] Starting compilation for {0}...</value>
|
||||||
|
</data>
|
||||||
|
<data name="script.compile.succeeded" xml:space="preserve">
|
||||||
|
<value>[Script] Compilation done with no errors.</value>
|
||||||
|
</data>
|
||||||
<data name="bot.scriptScheduler.loaded_task" xml:space="preserve">
|
<data name="bot.scriptScheduler.loaded_task" xml:space="preserve">
|
||||||
<value>Loaded task:
|
<value>Loaded task:
|
||||||
{0}</value>
|
{0}</value>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using System.ComponentModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
using MinecraftClient.Scripting.DynamicRun.Builder;
|
using MinecraftClient.Scripting.DynamicRun.Builder;
|
||||||
using static MinecraftClient.Settings;
|
using static MinecraftClient.Settings;
|
||||||
|
|
||||||
|
|
@ -16,6 +17,8 @@ namespace MinecraftClient.Scripting
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<ulong, byte[]> CompileCache = new();
|
private static readonly Dictionary<ulong, byte[]> CompileCache = new();
|
||||||
|
|
||||||
|
private readonly record struct ScriptSourceLine(int LineNumber, string Text);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Run the specified C# script file
|
/// Run the specified C# script file
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -48,12 +51,13 @@ namespace MinecraftClient.Scripting
|
||||||
{
|
{
|
||||||
//Process different sections of the script file
|
//Process different sections of the script file
|
||||||
bool scriptMain = true;
|
bool scriptMain = true;
|
||||||
List<string> script = new();
|
List<ScriptSourceLine> script = new();
|
||||||
List<string> extensions = new();
|
List<ScriptSourceLine> extensions = new();
|
||||||
List<string> libs = new();
|
List<string> libs = new();
|
||||||
List<string> dlls = new();
|
List<string> dlls = new();
|
||||||
foreach (string line in lines)
|
for (int i = 0; i < lines.Length; i++)
|
||||||
{
|
{
|
||||||
|
string line = lines[i];
|
||||||
if (line.StartsWith("//using"))
|
if (line.StartsWith("//using"))
|
||||||
{
|
{
|
||||||
libs.Add(line.Replace("//", "").Trim());
|
libs.Add(line.Replace("//", "").Trim());
|
||||||
|
|
@ -67,43 +71,18 @@ namespace MinecraftClient.Scripting
|
||||||
if (line.EndsWith("Extensions"))
|
if (line.EndsWith("Extensions"))
|
||||||
scriptMain = false;
|
scriptMain = false;
|
||||||
}
|
}
|
||||||
else if (scriptMain)
|
|
||||||
script.Add(line);
|
(scriptMain ? script : extensions).Add(new(i + 1, line));
|
||||||
else extensions.Add(line);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add return statement if missing
|
//Add return statement if missing
|
||||||
if (script.All(line => !line.StartsWith("return ") && !line.Contains(" return ")))
|
bool hasImplicitReturn = script.All(line => !line.Text.StartsWith("return ", StringComparison.Ordinal)
|
||||||
script.Add("return null;");
|
&& !line.Text.Contains(" return ", StringComparison.Ordinal));
|
||||||
|
|
||||||
//Generate a class from the given script
|
//Generate a class from the given script
|
||||||
string code = string.Join("\n", new string[]
|
string code = BuildScriptCode(scriptName, script, extensions, libs, hasImplicitReturn);
|
||||||
{
|
|
||||||
"using System;",
|
|
||||||
"using System.Collections.Generic;",
|
|
||||||
"using System.Text.RegularExpressions;",
|
|
||||||
"using System.Linq;",
|
|
||||||
"using System.Text;",
|
|
||||||
"using System.IO;",
|
|
||||||
"using System.Net;",
|
|
||||||
"using System.Threading;",
|
|
||||||
"using MinecraftClient;",
|
|
||||||
"using MinecraftClient.Scripting;",
|
|
||||||
"using MinecraftClient.Mapping;",
|
|
||||||
"using MinecraftClient.Inventory;",
|
|
||||||
string.Join("\n", libs),
|
|
||||||
"namespace ScriptLoader {",
|
|
||||||
"public class Script {",
|
|
||||||
"public CSharpAPI MCC;",
|
|
||||||
"public object __run(CSharpAPI __apiHandler, string[] args) {",
|
|
||||||
"this.MCC = __apiHandler;",
|
|
||||||
string.Join("\n", script),
|
|
||||||
"}",
|
|
||||||
string.Join("\n", extensions),
|
|
||||||
"}}",
|
|
||||||
});
|
|
||||||
|
|
||||||
ConsoleIO.WriteLogLine($"[Script] Starting compilation for {scriptName}...");
|
ConsoleIO.WriteLogLine(string.Format(Translations.script_compile_started, scriptName));
|
||||||
|
|
||||||
//Compile the C# class in memory using all the currently loaded assemblies
|
//Compile the C# class in memory using all the currently loaded assemblies
|
||||||
var result = compiler.Compile(code, Guid.NewGuid().ToString(), dlls);
|
var result = compiler.Compile(code, Guid.NewGuid().ToString(), dlls);
|
||||||
|
|
@ -112,22 +91,17 @@ namespace MinecraftClient.Scripting
|
||||||
if (result.Failures is not null)
|
if (result.Failures is not null)
|
||||||
{
|
{
|
||||||
|
|
||||||
ConsoleIO.WriteLogLine("[Script] Compilation failed with error(s):");
|
ConsoleIO.WriteLogLine(Translations.script_compile_failed);
|
||||||
|
|
||||||
foreach (var failure in result.Failures)
|
foreach (var failure in result.Failures)
|
||||||
{
|
{
|
||||||
// Get the line that contains the error:
|
ConsoleIO.WriteLogLine(FormatCompilationFailure(failure, scriptName));
|
||||||
|
|
||||||
var loc = failure.Location.GetMappedLineSpan();
|
|
||||||
var line = code.Split('\n')[loc.StartLinePosition.Line];
|
|
||||||
|
|
||||||
ConsoleIO.WriteLogLine($"[Script] Error in {scriptName}, on line ({line.Trim()}): [{failure.Id}] {failure.GetMessage()}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new CSharpException(CSErrorType.InvalidScript, new InvalidProgramException("Compilation failed due to error(s)."));
|
throw new CSharpException(CSErrorType.InvalidScript, new InvalidProgramException("Compilation failed due to error(s)."));
|
||||||
}
|
}
|
||||||
|
|
||||||
ConsoleIO.WriteLogLine("[Script] Compilation done with no errors.");
|
ConsoleIO.WriteLogLine(Translations.script_compile_succeeded);
|
||||||
|
|
||||||
//Retrieve compiled assembly
|
//Retrieve compiled assembly
|
||||||
assembly = result.Assembly;
|
assembly = result.Assembly;
|
||||||
|
|
@ -151,6 +125,77 @@ namespace MinecraftClient.Scripting
|
||||||
else return null;
|
else return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string BuildScriptCode(string scriptName, IEnumerable<ScriptSourceLine> script, IEnumerable<ScriptSourceLine> extensions, IEnumerable<string> libs, bool hasImplicitReturn)
|
||||||
|
{
|
||||||
|
StringBuilder codeBuilder = new();
|
||||||
|
codeBuilder.AppendLine("using System;");
|
||||||
|
codeBuilder.AppendLine("using System.Collections.Generic;");
|
||||||
|
codeBuilder.AppendLine("using System.Text.RegularExpressions;");
|
||||||
|
codeBuilder.AppendLine("using System.Linq;");
|
||||||
|
codeBuilder.AppendLine("using System.Text;");
|
||||||
|
codeBuilder.AppendLine("using System.IO;");
|
||||||
|
codeBuilder.AppendLine("using System.Net;");
|
||||||
|
codeBuilder.AppendLine("using System.Threading;");
|
||||||
|
codeBuilder.AppendLine("using MinecraftClient;");
|
||||||
|
codeBuilder.AppendLine("using MinecraftClient.Scripting;");
|
||||||
|
codeBuilder.AppendLine("using MinecraftClient.Mapping;");
|
||||||
|
codeBuilder.AppendLine("using MinecraftClient.Inventory;");
|
||||||
|
|
||||||
|
foreach (string lib in libs)
|
||||||
|
codeBuilder.AppendLine(lib);
|
||||||
|
|
||||||
|
codeBuilder.AppendLine("namespace ScriptLoader {");
|
||||||
|
codeBuilder.AppendLine("public class Script {");
|
||||||
|
codeBuilder.AppendLine("public CSharpAPI MCC;");
|
||||||
|
codeBuilder.AppendLine("public object __run(CSharpAPI __apiHandler, string[] args) {");
|
||||||
|
codeBuilder.AppendLine("this.MCC = __apiHandler;");
|
||||||
|
AppendMappedSection(codeBuilder, scriptName, script);
|
||||||
|
|
||||||
|
if (hasImplicitReturn)
|
||||||
|
codeBuilder.AppendLine("return null;");
|
||||||
|
|
||||||
|
codeBuilder.AppendLine("}");
|
||||||
|
AppendMappedSection(codeBuilder, scriptName, extensions);
|
||||||
|
codeBuilder.AppendLine("}}");
|
||||||
|
return codeBuilder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendMappedSection(StringBuilder codeBuilder, string scriptName, IEnumerable<ScriptSourceLine> lines)
|
||||||
|
{
|
||||||
|
ScriptSourceLine[] sourceLines = lines.ToArray();
|
||||||
|
if (sourceLines.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
codeBuilder.AppendLine($@"#line {sourceLines[0].LineNumber} ""{EscapeLineDirectivePath(scriptName)}""");
|
||||||
|
|
||||||
|
foreach (ScriptSourceLine line in sourceLines)
|
||||||
|
codeBuilder.AppendLine(line.Text);
|
||||||
|
|
||||||
|
codeBuilder.AppendLine("#line default");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string EscapeLineDirectivePath(string path)
|
||||||
|
{
|
||||||
|
return path.Replace("\\", "\\\\").Replace("\"", "\\\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatCompilationFailure(Diagnostic failure, string scriptName)
|
||||||
|
{
|
||||||
|
if (failure.Location.IsInSource)
|
||||||
|
{
|
||||||
|
var location = failure.Location.GetMappedLineSpan();
|
||||||
|
string sourcePath = string.IsNullOrWhiteSpace(location.Path) ? scriptName : location.Path;
|
||||||
|
return string.Format(Translations.script_compile_error,
|
||||||
|
sourcePath,
|
||||||
|
location.StartLinePosition.Line + 1,
|
||||||
|
location.StartLinePosition.Character + 1,
|
||||||
|
failure.Id,
|
||||||
|
failure.GetMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Format(Translations.script_compile_error_no_location, scriptName, failure.Id, failure.GetMessage());
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Quickly calculate a hash for the given script
|
/// Quickly calculate a hash for the given script
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue