mirror of
https://github.com/Stone-Red-Code/YesNt-Interpreter.git
synced 2026-09-04 00:56:31 +02:00
Add methods for removing and disabling statements, with related tests
This commit is contained in:
@@ -192,6 +192,81 @@ public class AddStatementTests
|
|||||||
YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement");
|
YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void RemoveStatementCausesInvalidStatementTest()
|
||||||
|
{
|
||||||
|
List<string> lines =
|
||||||
|
[
|
||||||
|
"sleep 100"
|
||||||
|
];
|
||||||
|
|
||||||
|
YesNtAssert.ContainsTerminationMessageWithSetup(lines, "Invalid statement", interpreter =>
|
||||||
|
{
|
||||||
|
interpreter.RemoveStatement("sleep");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void DisabledStatementIsIgnoredTest()
|
||||||
|
{
|
||||||
|
List<string> lines =
|
||||||
|
[
|
||||||
|
"var x = hello",
|
||||||
|
"${x}"
|
||||||
|
];
|
||||||
|
|
||||||
|
YesNtAssert.ContainsTerminationMessageWithSetup(lines, "Variable \"x\" not found", interpreter =>
|
||||||
|
{
|
||||||
|
interpreter.DisableStatement("var");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void DisabledStatementCanBeReenabledTest()
|
||||||
|
{
|
||||||
|
List<string> lines =
|
||||||
|
[
|
||||||
|
"var result = overwritten",
|
||||||
|
"${result}"
|
||||||
|
];
|
||||||
|
|
||||||
|
YesNtAssert.IsLastLineEqualWithSetup(lines, "overwritten", setup: interpreter =>
|
||||||
|
{
|
||||||
|
interpreter.DisableStatement("var");
|
||||||
|
interpreter.EnableStatement("var");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void DisableNonExistentStatementIsNoOpTest()
|
||||||
|
{
|
||||||
|
List<string> lines =
|
||||||
|
[
|
||||||
|
"var x = ok",
|
||||||
|
"${x}"
|
||||||
|
];
|
||||||
|
|
||||||
|
YesNtAssert.IsLastLineEqualWithSetup(lines, "ok", setup: interpreter =>
|
||||||
|
{
|
||||||
|
interpreter.DisableStatement("nonexistent_keyword");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void EnableNonDisabledStatementIsNoOpTest()
|
||||||
|
{
|
||||||
|
List<string> lines =
|
||||||
|
[
|
||||||
|
"var x = ok",
|
||||||
|
"${x}"
|
||||||
|
];
|
||||||
|
|
||||||
|
YesNtAssert.IsLastLineEqualWithSetup(lines, "ok", setup: interpreter =>
|
||||||
|
{
|
||||||
|
interpreter.EnableStatement("var");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void AddStatementWithHighPriorityRunsBeforeNormalTest()
|
public void AddStatementWithHighPriorityRunsBeforeNormalTest()
|
||||||
{
|
{
|
||||||
@@ -217,4 +292,46 @@ public class AddStatementTests
|
|||||||
|
|
||||||
Assert.IsTrue(highPriorityOrder < normalPriorityOrder, "High priority statement should execute before Normal priority");
|
Assert.IsTrue(highPriorityOrder < normalPriorityOrder, "High priority statement should execute before Normal priority");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void AddStatementAloneDoesNotReplaceBuiltinTest()
|
||||||
|
{
|
||||||
|
// Adding a custom 'var' handler without RemoveStatement first means BOTH handlers fire.
|
||||||
|
// The built-in still sets the variable, so ${x} resolves normally.
|
||||||
|
List<string> lines =
|
||||||
|
[
|
||||||
|
"var x = original",
|
||||||
|
"${x}"
|
||||||
|
];
|
||||||
|
|
||||||
|
bool customHandlerCalled = false;
|
||||||
|
|
||||||
|
YesNtAssert.IsLastLineEqualWithSetup(lines, "original", setup: interpreter =>
|
||||||
|
{
|
||||||
|
interpreter.AddStatement("var", SearchMode.StartOfLine, SpaceAround.End, _ =>
|
||||||
|
{
|
||||||
|
customHandlerCalled = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.IsTrue(customHandlerCalled, "Custom handler should have fired alongside the built-in");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void RemoveThenAddStatementReplacesBuiltinTest()
|
||||||
|
{
|
||||||
|
// RemoveStatement + AddStatement correctly replaces the built-in.
|
||||||
|
// The built-in 'var' is gone, so only the custom handler fires.
|
||||||
|
List<string> lines =
|
||||||
|
[
|
||||||
|
"var x = original",
|
||||||
|
"${x}"
|
||||||
|
];
|
||||||
|
|
||||||
|
YesNtAssert.ContainsTerminationMessageWithSetup(lines, "Variable \"x\" not found", interpreter =>
|
||||||
|
{
|
||||||
|
interpreter.RemoveStatement("var");
|
||||||
|
interpreter.AddStatement("var", SearchMode.StartOfLine, SpaceAround.End, _ => { });
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,12 @@ internal static class YesNtAssert
|
|||||||
StringAssert.Contains(debugOutput, expectedFragment);
|
StringAssert.Contains(debugOutput, expectedFragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void ContainsTerminationMessageWithSetup(List<string> lines, string expectedMessageFragment, Action<YesNtInterpreter> setup, int timeout = 1000)
|
||||||
|
{
|
||||||
|
(_, string debugOutput) = ExecuteAndCapture(lines, timeout, setup);
|
||||||
|
StringAssert.Contains(debugOutput, expectedMessageFragment);
|
||||||
|
}
|
||||||
|
|
||||||
public static string? GetLastLineWithSetup(List<string> lines, Action<YesNtInterpreter> setup, int timeout = 1000)
|
public static string? GetLastLineWithSetup(List<string> lines, Action<YesNtInterpreter> setup, int timeout = 1000)
|
||||||
{
|
{
|
||||||
(DebugEventArgs? debugEventArgs, _) = ExecuteAndCapture(lines, timeout, setup);
|
(DebugEventArgs? debugEventArgs, _) = ExecuteAndCapture(lines, timeout, setup);
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public class YesNtInterpreter
|
|||||||
private readonly RuntimeInformation runtimeInfo = new RuntimeInformation();
|
private readonly RuntimeInformation runtimeInfo = new RuntimeInformation();
|
||||||
private Dictionary<StatementAttribute, Action<string>> statements;
|
private Dictionary<StatementAttribute, Action<string>> statements;
|
||||||
private readonly List<KeyValuePair<StaticStatementAttribute, Action>> staticStatements;
|
private readonly List<KeyValuePair<StaticStatementAttribute, Action>> staticStatements;
|
||||||
|
private readonly Dictionary<string, List<KeyValuePair<StatementAttribute, Action<string>>>> disabledStatements = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a read-only snapshot of all currently registered statements.
|
/// Gets a read-only snapshot of all currently registered statements.
|
||||||
@@ -83,7 +84,10 @@ public class YesNtInterpreter
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers a custom statement using a pre-built <see cref="StatementAttribute"/>.
|
/// Registers a custom statement using a pre-built <see cref="StatementAttribute"/>.
|
||||||
/// If a statement with the same attribute key already exists it will be replaced.
|
/// If a statement with the same attribute key (identical field values) already exists it will be replaced;
|
||||||
|
/// otherwise a new entry is added. Built-in statements use distinct attribute instances, so passing a
|
||||||
|
/// newly constructed attribute with the same name will <b>add</b> a second handler rather than replacing
|
||||||
|
/// the built-in. Use <see cref="RemoveStatement"/> first to replace a built-in keyword.
|
||||||
/// The statement list is re-sorted by priority after insertion.
|
/// The statement list is re-sorted by priority after insertion.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="attribute">The attribute describing the keyword, search mode, and priority.</param>
|
/// <param name="attribute">The attribute describing the keyword, search mode, and priority.</param>
|
||||||
@@ -125,6 +129,71 @@ public class YesNtInterpreter
|
|||||||
AddStatement(new StatementAttribute(name, searchMode, spaceAround, consoleColor), handler);
|
AddStatement(new StatementAttribute(name, searchMode, spaceAround, consoleColor), handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Permanently removes all built-in or custom statements that match <paramref name="name"/>.
|
||||||
|
/// After removal, any script line that would have matched triggers an "Invalid statement" error.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The keyword of the statement(s) to remove.</param>
|
||||||
|
public void RemoveStatement(string name)
|
||||||
|
{
|
||||||
|
foreach (StatementAttribute key in statements.Keys.Where(k => k.Name == name).ToList())
|
||||||
|
{
|
||||||
|
statements.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
disabledStatements.Remove(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disables all statements matching <paramref name="name"/> by replacing their handlers with
|
||||||
|
/// a no-op. The keyword still matches (so no "Invalid statement" error is raised), but the
|
||||||
|
/// statement has no effect. Use <see cref="EnableStatement"/> to restore original behaviour.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The keyword of the statement(s) to disable.</param>
|
||||||
|
public void DisableStatement(string name)
|
||||||
|
{
|
||||||
|
if (disabledStatements.ContainsKey(name))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<KeyValuePair<StatementAttribute, Action<string>>> matching =
|
||||||
|
statements.Where(kv => kv.Key.Name == name).ToList();
|
||||||
|
|
||||||
|
if (matching.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
disabledStatements[name] = matching;
|
||||||
|
|
||||||
|
foreach (KeyValuePair<StatementAttribute, Action<string>> kv in matching)
|
||||||
|
{
|
||||||
|
statements[kv.Key] = _ => { };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Re-enables statements previously disabled with <see cref="DisableStatement"/>,
|
||||||
|
/// restoring their original handlers.
|
||||||
|
/// Has no effect if the statement is not currently disabled.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The keyword of the statement(s) to re-enable.</param>
|
||||||
|
public void EnableStatement(string name)
|
||||||
|
{
|
||||||
|
if (!disabledStatements.TryGetValue(name, out List<KeyValuePair<StatementAttribute, Action<string>>> saved))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (KeyValuePair<StatementAttribute, Action<string>> kv in saved)
|
||||||
|
{
|
||||||
|
statements[kv.Key] = kv.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
disabledStatements.Remove(name);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Requests a graceful stop of the currently executing script.
|
/// Requests a graceful stop of the currently executing script.
|
||||||
/// The interpreter will terminate at the next line boundary.
|
/// The interpreter will terminate at the next line boundary.
|
||||||
|
|||||||
+83
-3
@@ -12,9 +12,10 @@ to embed the YesNt interpreter and run scripts programmatically.
|
|||||||
3. [Running an in-memory script](#running-an-in-memory-script)
|
3. [Running an in-memory script](#running-an-in-memory-script)
|
||||||
4. [Capturing output (debug mode)](#capturing-output-debug-mode)
|
4. [Capturing output (debug mode)](#capturing-output-debug-mode)
|
||||||
5. [Adding custom statements](#adding-custom-statements)
|
5. [Adding custom statements](#adding-custom-statements)
|
||||||
6. [Stopping a script](#stopping-a-script)
|
6. [Removing and disabling built-in statements](#removing-and-disabling-built-in-statements)
|
||||||
7. [Reading registered statements](#reading-registered-statements)
|
7. [Stopping a script](#stopping-a-script)
|
||||||
8. [API reference](#api-reference)
|
8. [Reading registered statements](#reading-registered-statements)
|
||||||
|
9. [API reference](#api-reference)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -167,6 +168,76 @@ Use `StatementAttribute.Priority` to control ordering relative to built-in state
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Removing and disabling built-in statements
|
||||||
|
|
||||||
|
Use these methods to restrict which built-in keywords are available — useful for sandboxing
|
||||||
|
or replacing a built-in with a custom implementation.
|
||||||
|
|
||||||
|
### `RemoveStatement` — permanent removal
|
||||||
|
|
||||||
|
Removes all handlers for the given keyword. Any script line that would have matched the
|
||||||
|
keyword now triggers an **"Invalid statement"** error.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var interpreter = new YesNtInterpreter();
|
||||||
|
|
||||||
|
// Prevent scripts from launching external processes.
|
||||||
|
interpreter.RemoveStatement("exec");
|
||||||
|
|
||||||
|
interpreter.Execute(new List<string> { "exec notepad" });
|
||||||
|
// Terminates with: Invalid statement
|
||||||
|
```
|
||||||
|
|
||||||
|
### `DisableStatement` — silent no-op
|
||||||
|
|
||||||
|
Disables all handlers for the keyword. The keyword still **matches** (so no error is raised),
|
||||||
|
but has no effect. Use `EnableStatement` to restore the original behaviour.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var interpreter = new YesNtInterpreter();
|
||||||
|
|
||||||
|
// Make sleep a no-op so tests don't actually wait.
|
||||||
|
interpreter.DisableStatement("sleep");
|
||||||
|
|
||||||
|
interpreter.Execute(new List<string>
|
||||||
|
{
|
||||||
|
"sleep 10000", // does nothing
|
||||||
|
"var x = done",
|
||||||
|
"print_line ${x}", // prints: done
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `EnableStatement` — restore a disabled statement
|
||||||
|
|
||||||
|
Restores the original handlers saved when `DisableStatement` was called.
|
||||||
|
Has no effect if the statement is not currently disabled.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
interpreter.DisableStatement("sleep");
|
||||||
|
// ... configure other things ...
|
||||||
|
interpreter.EnableStatement("sleep"); // sleep works normally again
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replacing a built-in statement
|
||||||
|
|
||||||
|
Call `RemoveStatement` to remove the built-in handlers, then `AddStatement` to install your own.
|
||||||
|
Simply calling `AddStatement` with the same keyword name will **not** replace the built-in —
|
||||||
|
it will add a second handler that fires alongside the original.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Replace the built-in 'exec' with a sandboxed version that only allows 'echo'.
|
||||||
|
interpreter.RemoveStatement("exec");
|
||||||
|
interpreter.AddStatement("exec", SearchMode.StartOfLine, SpaceAround.End, args =>
|
||||||
|
{
|
||||||
|
if (args.Trim() != "echo")
|
||||||
|
throw new InvalidOperationException("exec is restricted");
|
||||||
|
|
||||||
|
System.Diagnostics.Process.Start("cmd", "/c echo (sandboxed)");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Stopping a script
|
## Stopping a script
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
@@ -253,6 +324,15 @@ public void AddStatement(StatementAttribute attribute, Action<string> handler);
|
|||||||
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action<string> handler);
|
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action<string> handler);
|
||||||
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color, Action<string> handler);
|
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color, Action<string> handler);
|
||||||
|
|
||||||
|
// Remove a built-in or custom statement permanently
|
||||||
|
public void RemoveStatement(string name);
|
||||||
|
|
||||||
|
// Disable a statement (silent no-op; reversible)
|
||||||
|
public void DisableStatement(string name);
|
||||||
|
|
||||||
|
// Re-enable a previously disabled statement
|
||||||
|
public void EnableStatement(string name);
|
||||||
|
|
||||||
// Request graceful stop
|
// Request graceful stop
|
||||||
public void Stop();
|
public void Stop();
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user