Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build-zxbstudio.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:

- name: Build project
run: dotnet build ZXBasicStudio.sln --configuration Release --no-restore

- name: Run tests
run: dotnet test

- name: Publish for Linux
run: |
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,7 @@ MigrationBackup/
.ionide/

# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd

# tmp/ dir
tmp/
40 changes: 16 additions & 24 deletions ZXBStudio/BuildSystem/ZXBasicMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ public ZXBasicMap(ZXCodeFile MainFile, IEnumerable<ZXCodeFile> AllFiles, string
ParseInputParameters(funcMatch.Groups[5].Value, currentFunction.InputParameters);

if (funcMatch.Groups[7].Success)
currentFunction.ReturnType = StorageFromString(funcMatch.Groups[5].Value, currentFunction.Name);
currentFunction.ReturnType = StorageFromString(funcMatch.Groups[7].Value, currentFunction.Name);
else
currentFunction.ReturnType = ZXVariableStorage.F;

Expand All @@ -202,8 +202,7 @@ public ZXBasicMap(ZXCodeFile MainFile, IEnumerable<ZXCodeFile> AllFiles, string
if (varNameDef.Contains("(")) //array
{
string varName = varNameDef.Substring(0, varNameDef.IndexOf("(")).Trim();

if (!jointLines.Skip(buc + 1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_]){varName}($|[^a-zA-Z0-9_])", RegexOptions.Multiline)))
if (!jointLines.Skip(buc + 1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_$]){Regex.Escape(varName)}($|[^a-zA-Z0-9_$])", RegexOptions.Multiline)))
continue;

string[] dims = varNameDef.Substring(varNameDef.IndexOf("(") + 1).Replace(")", "").Split(",", StringSplitOptions.RemoveEmptyEntries);
Expand All @@ -219,8 +218,7 @@ public ZXBasicMap(ZXCodeFile MainFile, IEnumerable<ZXCodeFile> AllFiles, string
foreach (var vName in varNames)
{
string varName = vName.Trim();

if (!jointLines.Skip(buc + 1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_]){varName}($|[^a-zA-Z0-9_])", RegexOptions.Multiline)))
if (!jointLines.Skip(buc + 1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_$]){Regex.Escape(varName)}($|[^a-zA-Z0-9_$])", RegexOptions.Multiline)))
continue;

var storage = StorageFromString(dimMatch.Groups[5].Value, varName);
Expand Down Expand Up @@ -291,15 +289,15 @@ public ZXBasicMap(ZXCodeFile MainFile, IEnumerable<ZXCodeFile> AllFiles, string
//Search for the var in the sub/function that the location points to
if (location.LocationType == ZXBasicLocationType.Sub)
{
var sub = subs.Where(s => s.Name == location.Name).FirstOrDefault();
var sub = subs.FirstOrDefault(s => string.Equals(s.Name, location.Name, StringComparison.OrdinalIgnoreCase));
if(sub != null)
foundVar = sub.LocalVariables.Where(v => v.Name == varName).FirstOrDefault();
foundVar = sub.LocalVariables.FirstOrDefault(v => string.Equals(v.Name, varName, StringComparison.OrdinalIgnoreCase));
}
else
{
var func = functions.Where(f => f.Name == location.Name).FirstOrDefault();
var func = functions.FirstOrDefault(f => string.Equals(f.Name, location.Name, StringComparison.OrdinalIgnoreCase));
if (func != null)
foundVar = func.LocalVariables.Where(v => v.Name == varName).FirstOrDefault();
foundVar = func.LocalVariables.FirstOrDefault(v => string.Equals(v.Name, varName, StringComparison.OrdinalIgnoreCase));
}
}

Expand All @@ -322,15 +320,15 @@ public ZXBasicMap(ZXCodeFile MainFile, IEnumerable<ZXCodeFile> AllFiles, string
//(to avoid the very unprobable case where the same var is defined in different files in locations that match the same range)
if (possibleLocation.LocationType == ZXBasicLocationType.Sub)
{
var sub = subs.Where(s => s.Name == possibleLocation.Name).FirstOrDefault();
var sub = subs.FirstOrDefault(s => string.Equals(s.Name, possibleLocation.Name, StringComparison.OrdinalIgnoreCase));
if (sub != null)
foundVar = sub.LocalVariables.Where(v => v.Name == varName && !v.Unused).FirstOrDefault();
foundVar = sub.LocalVariables.FirstOrDefault(v => string.Equals(v.Name, varName, StringComparison.OrdinalIgnoreCase) && !v.Unused);
}
else
{
var func = functions.Where(f => f.Name == possibleLocation.Name).FirstOrDefault();
var func = functions.FirstOrDefault(f => string.Equals(f.Name, possibleLocation.Name, StringComparison.OrdinalIgnoreCase));
if (func != null)
foundVar = func.LocalVariables.Where(v => v.Name == varName && !v.Unused).FirstOrDefault();
foundVar = func.LocalVariables.FirstOrDefault(v => string.Equals(v.Name, varName, StringComparison.OrdinalIgnoreCase) && !v.Unused);
}

//If the criteria finds a var, return it
Expand Down Expand Up @@ -359,11 +357,7 @@ void GetSubVars(ZXBasicSub Sub, string[] Lines)
if (varNameDef.Contains("(")) //array
{
string varName = varNameDef.Substring(0, varNameDef.IndexOf("(")).Trim();

//Ignore unused vars (vars that are found only on its dim line, there may be the improbable
//case where a var is defined and used in the same line using a colon and not used
//anywhere else, but that would be an awful code :) )
if (!Lines.Skip(buc+1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_]){varName}($|[^a-zA-Z0-9_])", RegexOptions.Multiline)))
if (!Lines.Skip(buc+1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_$]){Regex.Escape(varName)}($|[^a-zA-Z0-9_$])", RegexOptions.Multiline)))
continue;

string[] dims = varNameDef.Substring(varNameDef.IndexOf("(") + 1).Replace(")", "").Split(",", StringSplitOptions.RemoveEmptyEntries);
Expand All @@ -379,9 +373,7 @@ void GetSubVars(ZXBasicSub Sub, string[] Lines)
foreach (var vName in varNames)
{
string varName = vName.Trim();

//Ignore unused vars
if (!Lines.Skip(buc+1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_]){varName}($|[^a-zA-Z0-9_])", RegexOptions.Multiline)))
if (!Lines.Skip(buc+1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_$]){Regex.Escape(varName)}($|[^a-zA-Z0-9_$])", RegexOptions.Multiline)))
continue;

var storage = StorageFromString(dimMatch.Groups[5].Value, varName);
Expand Down Expand Up @@ -415,15 +407,15 @@ public List<ZXBasicLocation> GetBuildLocations(ZXCodeFile CodeFile)

if (subMatch != null && subMatch.Success)
{
loc = new ZXBasicLocation { Name = subMatch.Groups[2].Value.Trim(), LocationType = ZXBasicLocationType.Sub, FirstLine = buc, File = Path.Combine(CodeFile.Directory, CodeFile.TempFileName) };
loc = new ZXBasicLocation { Name = subMatch.Groups[4].Value.Trim(), LocationType = ZXBasicLocationType.Sub, FirstLine = buc, File = Path.Combine(CodeFile.Directory, CodeFile.TempFileName) };
continue;
}

var funcMatch = regFunc.Match(line);

if (funcMatch != null && funcMatch.Success)
{
loc = new ZXBasicLocation { Name = funcMatch.Groups[2].Value.Trim(), LocationType = ZXBasicLocationType.Function, FirstLine = buc, File = Path.Combine(CodeFile.Directory, CodeFile.TempFileName) };
loc = new ZXBasicLocation { Name = funcMatch.Groups[4].Value.Trim(), LocationType = ZXBasicLocationType.Function, FirstLine = buc, File = Path.Combine(CodeFile.Directory, CodeFile.TempFileName) };
continue;
}
}
Expand Down Expand Up @@ -465,7 +457,7 @@ public bool ContainsBuildDim(ZXCodeFile CodeFile, string VarName, int LineNumber
if (LineNumber >= lines.Length)
return false;

return Regex.IsMatch(lines[LineNumber], $"(\\s|,){VarName}(\\s|,|\\(|$)", RegexOptions.Multiline);
return Regex.IsMatch(lines[LineNumber], $"(\\s|,){Regex.Escape(VarName)}(\\s|,|\\(|$)", RegexOptions.Multiline);
}

private static void ParseInputParameters(string ParameterString, List<ZXBasicParameter> Storage)
Expand Down
17 changes: 8 additions & 9 deletions ZXBStudio/BuildSystem/ZXVariableMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public ZXVariableMap(string ICFile, string MapFile, ZXBasicMap BasicMap)
}


private void ProcessGlobalVariables(string icContent, string mapContent, ZXBasicMap BasicMap)
internal void ProcessGlobalVariables(string icContent, string mapContent, ZXBasicMap BasicMap)
{
int splitIndex = icContent.IndexOf("--- end of user code ---");

Expand All @@ -60,7 +60,7 @@ private void ProcessGlobalVariables(string icContent, string mapContent, ZXBasic
string varName = m.Groups[1].Value;
string bVarName = varName.Substring(1);

var basicVar = BasicMap.GlobalVariables.FirstOrDefault(v => v.Name == bVarName);
var basicVar = BasicMap.GlobalVariables.FirstOrDefault(v => string.Equals(v.Name.TrimEnd('$'), bVarName.TrimEnd('$'), StringComparison.OrdinalIgnoreCase));

if (basicVar == null)
continue;
Expand All @@ -78,7 +78,7 @@ private void ProcessGlobalVariables(string icContent, string mapContent, ZXBasic

ZXVariable newVar = new ZXVariable
{
Name = bVarName,
Name = basicVar.Name,
Address = new ZXVariableAddress { AddressType = ZXVariableAddressType.Absolute, AddressValue = addr },
Scope = ZXVariableScope.GlobalScope,
VariableType = ZXVariableType.Flat,
Expand All @@ -98,7 +98,7 @@ private void ProcessGlobalVariables(string icContent, string mapContent, ZXBasic

string bVarName = varName.Substring(1);

var basicVar = BasicMap.GlobalVariables.FirstOrDefault(v => v.Name == bVarName);
var basicVar = BasicMap.GlobalVariables.FirstOrDefault(v => string.Equals(v.Name.TrimEnd('$'), bVarName.TrimEnd('$'), StringComparison.OrdinalIgnoreCase));

if (basicVar == null)
continue;
Expand Down Expand Up @@ -133,7 +133,7 @@ private void ProcessGlobalVariables(string icContent, string mapContent, ZXBasic

ZXVariable newVar = new ZXVariable
{
Name = bVarName,
Name = basicVar.Name,
Address = new ZXVariableAddress { AddressType = ZXVariableAddressType.Absolute, AddressValue = addr },
Scope = ZXVariableScope.GlobalScope,
VariableType = ZXVariableType.Array,
Expand All @@ -144,7 +144,7 @@ private void ProcessGlobalVariables(string icContent, string mapContent, ZXBasic
}
}

private void ProcessLocalVariables(string icContent, string mapContent, ZXBasicMap BasicMap)
internal void ProcessLocalVariables(string icContent, string mapContent, ZXBasicMap BasicMap)
{
int splitIndex = icContent.IndexOf("--- end of user code ---");

Expand Down Expand Up @@ -179,10 +179,9 @@ private void ProcessLocalVariables(string icContent, string mapContent, ZXBasicM

ZXVariableScope currentScope = new ZXVariableScope { ScopeName = locName, StartAddress = startAddr, EndAddress = endAddr };

ZXBasicSub? sub = BasicMap.Subs.Where(m => m.Name == locName).FirstOrDefault();

ZXBasicSub? sub = BasicMap.Subs.FirstOrDefault(m => string.Equals(m.Name.TrimEnd('$'), locName.TrimEnd('$'), StringComparison.OrdinalIgnoreCase));
if (sub == null)
sub = BasicMap.Functions.Where(m => m.Name == locName).FirstOrDefault();
sub = BasicMap.Functions.FirstOrDefault(m => string.Equals(m.Name.TrimEnd('$'), locName.TrimEnd('$'), StringComparison.OrdinalIgnoreCase));

//Function params
if (sub != null)
Expand Down
4 changes: 4 additions & 0 deletions ZXBStudio/ZXBasicStudio.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
<Deterministic>False</Deterministic>
<Version>1.7.0.0</Version>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="ZXBasicStudioTest" />
</ItemGroup>
<ItemGroup>
<AvaloniaXaml Remove="LanguageDefinitions\**" />
<Compile Remove="LanguageDefinitions\**" />
Expand Down
123 changes: 123 additions & 0 deletions ZXBasicStudioTest/SigilMappingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using Xunit;
using FluentAssertions;
using ZXBasicStudio.BuildSystem;
using System.Runtime.Serialization;
using System.Collections.Generic;
using System.Linq;

namespace ZXBasicStudioTest
{
public class SigilMappingTests
{
[Fact]
public void GetVariables_ShouldMatchSigilVariable()
{
// Arrange
// We use GetUninitializedObject to skip the constructor which depends on files
var basicMap = (ZXBasicMap)FormatterServices.GetUninitializedObject(typeof(ZXBasicMap));

basicMap.GlobalVariables = new[]
{
new ZXBasicVariable { Name = "a$" }
};
basicMap.Subs = new ZXBasicSub[0];
basicMap.Functions = new ZXBasicFunction[0];
basicMap.BuildLocations = new ZXBasicLocation[0];

// IC Content mimicking a global variable '_a'
string icContent = "--- end of user code ---\n('var', '_a', '0')";
// Map content mimicking the same variable
string mapContent = "8000: ._a";

// Act
var variableMap = (ZXVariableMap)FormatterServices.GetUninitializedObject(typeof(ZXVariableMap));
// We need to initialize the private 'vars' list since we used GetUninitializedObject
var varsField = typeof(ZXVariableMap).GetField("vars", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
varsField!.SetValue(variableMap, new List<ZXVariable>());

variableMap.ProcessGlobalVariables(icContent, mapContent, basicMap);
var variables = variableMap.Variables;

// Assert
variables.Should().NotBeNull();
var variable = variables.FirstOrDefault(v => v.Name == "a$");
variable.Should().NotBeNull("Variable 'a$' should be found even if IC uses '_a'");
variable!.Address.AddressValue.Should().Be(0x8000);
}

[Fact]
public void GetVariables_ShouldBeCaseInsensitive()
{
// Arrange
var basicMap = (ZXBasicMap)FormatterServices.GetUninitializedObject(typeof(ZXBasicMap));

basicMap.GlobalVariables = new[]
{
new ZXBasicVariable { Name = "MyVar$" }
};
basicMap.Subs = new ZXBasicSub[0];
basicMap.Functions = new ZXBasicFunction[0];
basicMap.BuildLocations = new ZXBasicLocation[0];

// IC Content uses lowercase '_myvar'
string icContent = "--- end of user code ---\n('var', '_myvar', '0')";
string mapContent = "9000: ._myvar";

// Act
var variableMap = (ZXVariableMap)FormatterServices.GetUninitializedObject(typeof(ZXVariableMap));
var varsField = typeof(ZXVariableMap).GetField("vars", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
varsField!.SetValue(variableMap, new List<ZXVariable>());

variableMap.ProcessGlobalVariables(icContent, mapContent, basicMap);
var variables = variableMap.Variables;

// Assert
var variable = variables.FirstOrDefault(v => v.Name == "MyVar$");
variable.Should().NotBeNull("Variable 'MyVar$' should be matched case-insensitively");
variable!.Address.AddressValue.Should().Be(0x9000);
}

[Fact]
public void ProcessLocalVariables_ShouldMatchSubNameWithSigil()
{
// Arrange
var basicMap = (ZXBasicMap)FormatterServices.GetUninitializedObject(typeof(ZXBasicMap));

var sub = new ZXBasicSub { Name = "MySub$" };
sub.LocalVariables = new List<ZXBasicVariable>();
sub.InputParameters = new List<ZXBasicParameter>
{
new ZXBasicParameter { Name = "param1", Offset = -2, Storage = ZXVariableStorage.U16 }
};

basicMap.GlobalVariables = new ZXBasicVariable[0];
basicMap.Subs = new[] { sub };
basicMap.Functions = new ZXBasicFunction[0];
basicMap.BuildLocations = new[]
{
new ZXBasicLocation { Name = "MySub$", LocationType = ZXBasicLocationType.Sub, FirstLine = 0, LastLine = 10, File = "main.bas" }
};

// IC Content showing start and end of MySub (sigil is stripped in label usually: _MySub)
// Note: ProcessLocalVariables extracts locName = label.Substring(1) from '_MySub' -> 'MySub'
string icContent = "('label', '_MySub')\n('label', '_MySub__leave')\n--- end of user code ---";
// Map content showing start and end addresses
string mapContent = "8000: ._MySub\n8010: ._MySub__leave";

// Act
var variableMap = (ZXVariableMap)FormatterServices.GetUninitializedObject(typeof(ZXVariableMap));
var varsField = typeof(ZXVariableMap).GetField("vars", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
varsField!.SetValue(variableMap, new List<ZXVariable>());

variableMap.ProcessLocalVariables(icContent, mapContent, basicMap);
var variables = variableMap.Variables;

// Assert
variables.Should().NotBeNull();
// If MySub$ was matched correctly, its parameters should be added
variables.Should().Contain(v => v.Name == "param1", "Sub MySub$ should be matched to label _MySub and its parameters processed");
var param = variables.First(v => v.Name == "param1");
param.Scope.ScopeName.Should().Be("MySub");
}
}
}
14 changes: 0 additions & 14 deletions ZXBasicStudioTest/UnitTest1.cs

This file was deleted.

Loading
Loading