Skip to content

Commit

Permalink
Проверка инициализации переменной +semver:feature (#98)
Browse files Browse the repository at this point in the history
* fix

* Контракт exit codes

* изменение области видимости

* #84 - подготовил интеграционники для скриптов с семантическими ошибками

* #84 - реализация фичи

* #84 - подправил тесты
  • Loading branch information
Stepami authored Aug 12, 2024
1 parent 4ca963b commit de4e279
Show file tree
Hide file tree
Showing 16 changed files with 137 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.PrimaryExpressions;

namespace HydraScript.Application.StaticAnalysis.Exceptions;

[ExcludeFromCodeCoverage]
public class AccessBeforeInitialization(IdentifierReference variable) : SemanticException(
variable.Segment,
$"Cannot access '{variable.Name}' before initialization");
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public VisitUnit Visit(FunctionDeclaration visitable)
var arg = new VariableSymbol(
id: x.Key,
x.TypeValue.Accept(_typeBuilder));
arg.Initialize();
_symbolTables[visitable.Scope].AddSymbol(arg);
return arg;
}).ToList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ public Type Visit(ExpressionStatement visitable) =>
public Type Visit(IdentifierReference visitable)
{
var symbol = _symbolTables[visitable.Scope].FindSymbol<ISymbol>(visitable.Name);
if (symbol is { Initialized: false })
throw new AccessBeforeInitialization(visitable);
return symbol?.Type ?? throw new UnknownIdentifierReference(visitable);
}

Expand Down Expand Up @@ -167,11 +169,13 @@ public Type Visit(ObjectLiteral visitable)
var properties = visitable.Properties.Select(prop =>
{
var propType = prop.Expression.Accept(This);
_symbolTables[visitable.Scope].AddSymbol(propType switch
var propSymbol = propType switch
{
ObjectType objectType => new ObjectSymbol(prop.Id, objectType),
_ => new VariableSymbol(prop.Id, propType)
});
};
propSymbol.Initialize();
_symbolTables[visitable.Scope].AddSymbol(propSymbol);
return new PropertyType(prop.Id, propType);
});
var objectLiteralType = new ObjectType(properties);
Expand Down Expand Up @@ -282,6 +286,7 @@ public Type Visit(LexicalDeclaration visitable)
ObjectType objectType => new ObjectSymbol(registeredSymbol.Id, objectType, visitable.ReadOnly),
_ => new VariableSymbol(registeredSymbol.Id, actualType, visitable.ReadOnly)
};
actualSymbol.Initialize();
_symbolTables[visitable.Scope].AddSymbol(actualSymbol);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public partial class ImplicitLiteral(TypeValue type) : AbstractLiteral(type)
public object? ComputedDefaultValue { private get; set; }

protected override string NodeRepresentation() =>
Type.ToString();
$"Implicit {Type}";

public override ValueDto ToValueDto() =>
ValueDto.ConstantDto(
Expand Down
1 change: 1 addition & 0 deletions src/Domain/HydraScript.Domain.IR/ISymbol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public interface ISymbol
{
public string Id { get; }
public Type Type { get; }
public bool Initialized { get; }
}
1 change: 1 addition & 0 deletions src/Domain/HydraScript.Domain.IR/Impl/Symbols/Symbol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public abstract class Symbol(string id, Type type) : ISymbol
{
public virtual string Id { get; } = id;
public virtual Type Type { get; } = type;
public virtual bool Initialized => true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ public class VariableSymbol(
Type type,
bool readOnly = false) : Symbol(id, type)
{
private bool _initialized = readOnly;

public bool ReadOnly { get; } = readOnly;
public override bool Initialized => _initialized;

public void Initialize() => _initialized = true;

public override string ToString() =>
$"{(ReadOnly ? "const " : "")}{Id}: {Type}";
Expand Down
8 changes: 4 additions & 4 deletions src/HydraScript/ExecuteCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace HydraScript;

public class ExecuteCommand : RootCommand
internal class ExecuteCommand : RootCommand
{
public ExecuteCommand() : base("HydraScript interpreter")
internal ExecuteCommand() : base("HydraScript interpreter")
{
PathArgument = new Argument<FileInfo>(
name: "path",
Expand All @@ -18,6 +18,6 @@ public ExecuteCommand() : base("HydraScript interpreter")
AddOption(DumpOption);
}

public Argument<FileInfo> PathArgument { get; }
public Option<bool> DumpOption { get; }
internal Argument<FileInfo> PathArgument { get; }
internal Option<bool> DumpOption { get; }
}
6 changes: 3 additions & 3 deletions src/HydraScript/ExecuteCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ public int Invoke(InvocationContext context)
var ast = parser.Parse(sourceCode);
var instructions = codeGenerator.GetInstructions(ast);
virtualMachine.Run(instructions);
return 0;
return ExitCodes.Success;
}
catch (Exception ex)
when (ex is LexerException or ParserException or SemanticException)
{
writer.WriteLine(ex.Message);
return 1;
return ExitCodes.HydraScriptError;
}
catch (Exception ex)
{
writer.WriteLine("Internal HydraScript Error");
writer.WriteLine(ex);
return 2;
return ExitCodes.DotnetRuntimeError;
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/HydraScript/ExitCodes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace HydraScript;

internal static class ExitCodes
{
public const int Success = 0;

public const int HydraScriptError = 1;

public const int DotnetRuntimeError = 2;
}
6 changes: 3 additions & 3 deletions src/HydraScript/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@

return GetRunner(ConfigureHost).Invoke(args);

public static partial class Program
internal static partial class Program
{
public static readonly ExecuteCommand Command = new();
internal static readonly ExecuteCommand Command = new();

public static Parser GetRunner(Action<IHostBuilder> configureHost, bool useDefault = true)
internal static Parser GetRunner(Action<IHostBuilder> configureHost, bool useDefault = true)
{
var builder = new CommandLineBuilder(Command)
.UseHost(Host.CreateDefaultBuilder, configureHost);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.CommandLine.Parsing;
using FluentAssertions;
using Xunit.Abstractions;

namespace HydraScript.IntegrationTests.ErrorPrograms;

public class VariableInitializationTests(
TestHostFixture fixture,
ITestOutputHelper testOutputHelper) : IClassFixture<TestHostFixture>, IDisposable
{
private readonly StringWriter _writer = new();

[Fact]
public void VariableWithoutTypeDeclared_AccessedBeforeInitialization_ExitCodeHydraScriptError()
{
const string script =
"""
let x = f()
function f() {
print(x as string)
return 5
}
""";
var runner = fixture.GetRunner(
testOutputHelper,
_writer,
configureTestServices: services => services.SetupInMemoryScript(script));
var code = runner.Invoke(fixture.InMemoryScript);
code.Should().Be(ExitCodes.HydraScriptError);
var output = _writer.ToString().Trim();
output.Should().Be("(3, 11)-(3, 12) Cannot access 'x' before initialization");
}

[Fact]
public void TypedVariableDeclared_AccessedBeforeInitialization_ExitCodeHydraScriptError()
{
const string script =
"""
let x: number = f()
function f() {
print(x as string)
return 5
}
""";
var runner = fixture.GetRunner(
testOutputHelper,
_writer,
configureTestServices: services => services.SetupInMemoryScript(script));
var code = runner.Invoke(fixture.InMemoryScript);
code.Should().Be(ExitCodes.HydraScriptError);
var output = _writer.ToString().Trim();
output.Should().Be("(3, 11)-(3, 12) Cannot access 'x' before initialization");
}

public void Dispose()
{
_writer.Dispose();
fixture.Dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<PackageReference Include="FluentAssertions" Version="6.12.0"/>
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0"/>
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="xunit" Version="2.9.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.IO.Abstractions;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;

namespace HydraScript.IntegrationTests;

internal static class ServiceCollectionTestExtensions
{
internal static void SetupInMemoryScript(this IServiceCollection services, string script)
{
var fileSystem = Substitute.For<IFileSystem>();
var file = Substitute.For<IFile>();
file.ReadAllText(default!).ReturnsForAnyArgs(script);
fileSystem.File.ReturnsForAnyArgs(file);
services.AddSingleton(fileSystem);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ public void Invoke_NoError_ReturnCodeIsZero(string fileName)
{
var runner = fixture.GetRunner(testOutputHelper);
var code = runner.Invoke([$"Samples/{fileName}"]);
testOutputHelper.WriteLine(fixture.Writer.ToString());
code.Should().Be(0);
code.Should().Be(ExitCodes.Success);
}

public static TheoryData<string> SuccessfulProgramsNames =>
Expand Down
21 changes: 13 additions & 8 deletions tests/HydraScript.IntegrationTests/TestHostFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ namespace HydraScript.IntegrationTests;

public class TestHostFixture : IDisposable
{
public readonly TextWriter Writer = new StringWriter();
public readonly string[] InMemoryScript = ["file.js"];

public Parser GetRunner(ITestOutputHelper testOutputHelper) =>
public Parser GetRunner(
ITestOutputHelper testOutputHelper,
TextWriter? writer = null,
Action<IServiceCollection>? configureTestServices = null) =>
Program.GetRunner(configureHost: builder => builder
.ConfigureLogging(x =>
{
Expand All @@ -21,17 +24,19 @@ public Parser GetRunner(ITestOutputHelper testOutputHelper) =>
})
.ConfigureServices((context, services) =>
{
var parseResult = context.GetInvocationContext().ParseResult;
var fileInfo = parseResult.GetValueForArgument(Program.Command.PathArgument);
var dump = parseResult.GetValueForOption(Program.Command.DumpOption);
var fileInfo = context.GetInvocationContext().ParseResult
.GetValueForArgument(Program.Command.PathArgument);
services
.AddDomain()
.AddApplication()
.AddInfrastructure(dump, fileInfo);
services.AddSingleton(Writer);
.AddInfrastructure(dump: false, fileInfo);
services.AddSingleton(writer ?? TextWriter.Null);
configureTestServices?.Invoke(services);
})
.UseCommandHandler<ExecuteCommand, ExecuteCommandHandler>(),
useDefault: false);

public void Dispose() => Writer.Dispose();
public void Dispose()
{
}
}

0 comments on commit de4e279

Please sign in to comment.