Skip to content

Commit de4e279

Browse files
authored
Проверка инициализации переменной +semver:feature (#98)
* fix * Контракт exit codes * изменение области видимости * #84 - подготовил интеграционники для скриптов с семантическими ошибками * #84 - реализация фичи * #84 - подправил тесты
1 parent 4ca963b commit de4e279

File tree

16 files changed

+137
-23
lines changed

16 files changed

+137
-23
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.PrimaryExpressions;
3+
4+
namespace HydraScript.Application.StaticAnalysis.Exceptions;
5+
6+
[ExcludeFromCodeCoverage]
7+
public class AccessBeforeInitialization(IdentifierReference variable) : SemanticException(
8+
variable.Segment,
9+
$"Cannot access '{variable.Name}' before initialization");

src/Application/HydraScript.Application.StaticAnalysis/Visitors/DeclarationVisitor.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public VisitUnit Visit(FunctionDeclaration visitable)
7474
var arg = new VariableSymbol(
7575
id: x.Key,
7676
x.TypeValue.Accept(_typeBuilder));
77+
arg.Initialize();
7778
_symbolTables[visitable.Scope].AddSymbol(arg);
7879
return arg;
7980
}).ToList();

src/Application/HydraScript.Application.StaticAnalysis/Visitors/SemanticChecker.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ public Type Visit(ExpressionStatement visitable) =>
137137
public Type Visit(IdentifierReference visitable)
138138
{
139139
var symbol = _symbolTables[visitable.Scope].FindSymbol<ISymbol>(visitable.Name);
140+
if (symbol is { Initialized: false })
141+
throw new AccessBeforeInitialization(visitable);
140142
return symbol?.Type ?? throw new UnknownIdentifierReference(visitable);
141143
}
142144

@@ -167,11 +169,13 @@ public Type Visit(ObjectLiteral visitable)
167169
var properties = visitable.Properties.Select(prop =>
168170
{
169171
var propType = prop.Expression.Accept(This);
170-
_symbolTables[visitable.Scope].AddSymbol(propType switch
172+
var propSymbol = propType switch
171173
{
172174
ObjectType objectType => new ObjectSymbol(prop.Id, objectType),
173175
_ => new VariableSymbol(prop.Id, propType)
174-
});
176+
};
177+
propSymbol.Initialize();
178+
_symbolTables[visitable.Scope].AddSymbol(propSymbol);
175179
return new PropertyType(prop.Id, propType);
176180
});
177181
var objectLiteralType = new ObjectType(properties);
@@ -282,6 +286,7 @@ public Type Visit(LexicalDeclaration visitable)
282286
ObjectType objectType => new ObjectSymbol(registeredSymbol.Id, objectType, visitable.ReadOnly),
283287
_ => new VariableSymbol(registeredSymbol.Id, actualType, visitable.ReadOnly)
284288
};
289+
actualSymbol.Initialize();
285290
_symbolTables[visitable.Scope].AddSymbol(actualSymbol);
286291
}
287292

src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/ImplicitLiteral.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public partial class ImplicitLiteral(TypeValue type) : AbstractLiteral(type)
88
public object? ComputedDefaultValue { private get; set; }
99

1010
protected override string NodeRepresentation() =>
11-
Type.ToString();
11+
$"Implicit {Type}";
1212

1313
public override ValueDto ToValueDto() =>
1414
ValueDto.ConstantDto(

src/Domain/HydraScript.Domain.IR/ISymbol.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ public interface ISymbol
44
{
55
public string Id { get; }
66
public Type Type { get; }
7+
public bool Initialized { get; }
78
}

src/Domain/HydraScript.Domain.IR/Impl/Symbols/Symbol.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ public abstract class Symbol(string id, Type type) : ISymbol
44
{
55
public virtual string Id { get; } = id;
66
public virtual Type Type { get; } = type;
7+
public virtual bool Initialized => true;
78
}

src/Domain/HydraScript.Domain.IR/Impl/Symbols/VariableSymbol.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ public class VariableSymbol(
55
Type type,
66
bool readOnly = false) : Symbol(id, type)
77
{
8+
private bool _initialized = readOnly;
9+
810
public bool ReadOnly { get; } = readOnly;
11+
public override bool Initialized => _initialized;
12+
13+
public void Initialize() => _initialized = true;
914

1015
public override string ToString() =>
1116
$"{(ReadOnly ? "const " : "")}{Id}: {Type}";

src/HydraScript/ExecuteCommand.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
namespace HydraScript;
44

5-
public class ExecuteCommand : RootCommand
5+
internal class ExecuteCommand : RootCommand
66
{
7-
public ExecuteCommand() : base("HydraScript interpreter")
7+
internal ExecuteCommand() : base("HydraScript interpreter")
88
{
99
PathArgument = new Argument<FileInfo>(
1010
name: "path",
@@ -18,6 +18,6 @@ public ExecuteCommand() : base("HydraScript interpreter")
1818
AddOption(DumpOption);
1919
}
2020

21-
public Argument<FileInfo> PathArgument { get; }
22-
public Option<bool> DumpOption { get; }
21+
internal Argument<FileInfo> PathArgument { get; }
22+
internal Option<bool> DumpOption { get; }
2323
}

src/HydraScript/ExecuteCommandHandler.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,19 @@ public int Invoke(InvocationContext context)
2424
var ast = parser.Parse(sourceCode);
2525
var instructions = codeGenerator.GetInstructions(ast);
2626
virtualMachine.Run(instructions);
27-
return 0;
27+
return ExitCodes.Success;
2828
}
2929
catch (Exception ex)
3030
when (ex is LexerException or ParserException or SemanticException)
3131
{
3232
writer.WriteLine(ex.Message);
33-
return 1;
33+
return ExitCodes.HydraScriptError;
3434
}
3535
catch (Exception ex)
3636
{
3737
writer.WriteLine("Internal HydraScript Error");
3838
writer.WriteLine(ex);
39-
return 2;
39+
return ExitCodes.DotnetRuntimeError;
4040
}
4141
}
4242

src/HydraScript/ExitCodes.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace HydraScript;
2+
3+
internal static class ExitCodes
4+
{
5+
public const int Success = 0;
6+
7+
public const int HydraScriptError = 1;
8+
9+
public const int DotnetRuntimeError = 2;
10+
}

src/HydraScript/Program.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99

1010
return GetRunner(ConfigureHost).Invoke(args);
1111

12-
public static partial class Program
12+
internal static partial class Program
1313
{
14-
public static readonly ExecuteCommand Command = new();
14+
internal static readonly ExecuteCommand Command = new();
1515

16-
public static Parser GetRunner(Action<IHostBuilder> configureHost, bool useDefault = true)
16+
internal static Parser GetRunner(Action<IHostBuilder> configureHost, bool useDefault = true)
1717
{
1818
var builder = new CommandLineBuilder(Command)
1919
.UseHost(Host.CreateDefaultBuilder, configureHost);
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System.CommandLine.Parsing;
2+
using FluentAssertions;
3+
using Xunit.Abstractions;
4+
5+
namespace HydraScript.IntegrationTests.ErrorPrograms;
6+
7+
public class VariableInitializationTests(
8+
TestHostFixture fixture,
9+
ITestOutputHelper testOutputHelper) : IClassFixture<TestHostFixture>, IDisposable
10+
{
11+
private readonly StringWriter _writer = new();
12+
13+
[Fact]
14+
public void VariableWithoutTypeDeclared_AccessedBeforeInitialization_ExitCodeHydraScriptError()
15+
{
16+
const string script =
17+
"""
18+
let x = f()
19+
function f() {
20+
print(x as string)
21+
return 5
22+
}
23+
""";
24+
var runner = fixture.GetRunner(
25+
testOutputHelper,
26+
_writer,
27+
configureTestServices: services => services.SetupInMemoryScript(script));
28+
var code = runner.Invoke(fixture.InMemoryScript);
29+
code.Should().Be(ExitCodes.HydraScriptError);
30+
var output = _writer.ToString().Trim();
31+
output.Should().Be("(3, 11)-(3, 12) Cannot access 'x' before initialization");
32+
}
33+
34+
[Fact]
35+
public void TypedVariableDeclared_AccessedBeforeInitialization_ExitCodeHydraScriptError()
36+
{
37+
const string script =
38+
"""
39+
let x: number = f()
40+
function f() {
41+
print(x as string)
42+
return 5
43+
}
44+
""";
45+
var runner = fixture.GetRunner(
46+
testOutputHelper,
47+
_writer,
48+
configureTestServices: services => services.SetupInMemoryScript(script));
49+
var code = runner.Invoke(fixture.InMemoryScript);
50+
code.Should().Be(ExitCodes.HydraScriptError);
51+
var output = _writer.ToString().Trim();
52+
output.Should().Be("(3, 11)-(3, 12) Cannot access 'x' before initialization");
53+
}
54+
55+
public void Dispose()
56+
{
57+
_writer.Dispose();
58+
fixture.Dispose();
59+
}
60+
}

tests/HydraScript.IntegrationTests/HydraScript.IntegrationTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<PackageReference Include="FluentAssertions" Version="6.12.0"/>
1313
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.4.0" />
1414
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0"/>
15+
<PackageReference Include="NSubstitute" Version="5.1.0" />
1516
<PackageReference Include="xunit" Version="2.9.0"/>
1617
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
1718
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.IO.Abstractions;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using NSubstitute;
4+
5+
namespace HydraScript.IntegrationTests;
6+
7+
internal static class ServiceCollectionTestExtensions
8+
{
9+
internal static void SetupInMemoryScript(this IServiceCollection services, string script)
10+
{
11+
var fileSystem = Substitute.For<IFileSystem>();
12+
var file = Substitute.For<IFile>();
13+
file.ReadAllText(default!).ReturnsForAnyArgs(script);
14+
fileSystem.File.ReturnsForAnyArgs(file);
15+
services.AddSingleton(fileSystem);
16+
}
17+
}

tests/HydraScript.IntegrationTests/SuccessfulProgramsTests.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ public void Invoke_NoError_ReturnCodeIsZero(string fileName)
1313
{
1414
var runner = fixture.GetRunner(testOutputHelper);
1515
var code = runner.Invoke([$"Samples/{fileName}"]);
16-
testOutputHelper.WriteLine(fixture.Writer.ToString());
17-
code.Should().Be(0);
16+
code.Should().Be(ExitCodes.Success);
1817
}
1918

2019
public static TheoryData<string> SuccessfulProgramsNames =>

tests/HydraScript.IntegrationTests/TestHostFixture.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ namespace HydraScript.IntegrationTests;
1010

1111
public class TestHostFixture : IDisposable
1212
{
13-
public readonly TextWriter Writer = new StringWriter();
13+
public readonly string[] InMemoryScript = ["file.js"];
1414

15-
public Parser GetRunner(ITestOutputHelper testOutputHelper) =>
15+
public Parser GetRunner(
16+
ITestOutputHelper testOutputHelper,
17+
TextWriter? writer = null,
18+
Action<IServiceCollection>? configureTestServices = null) =>
1619
Program.GetRunner(configureHost: builder => builder
1720
.ConfigureLogging(x =>
1821
{
@@ -21,17 +24,19 @@ public Parser GetRunner(ITestOutputHelper testOutputHelper) =>
2124
})
2225
.ConfigureServices((context, services) =>
2326
{
24-
var parseResult = context.GetInvocationContext().ParseResult;
25-
var fileInfo = parseResult.GetValueForArgument(Program.Command.PathArgument);
26-
var dump = parseResult.GetValueForOption(Program.Command.DumpOption);
27+
var fileInfo = context.GetInvocationContext().ParseResult
28+
.GetValueForArgument(Program.Command.PathArgument);
2729
services
2830
.AddDomain()
2931
.AddApplication()
30-
.AddInfrastructure(dump, fileInfo);
31-
services.AddSingleton(Writer);
32+
.AddInfrastructure(dump: false, fileInfo);
33+
services.AddSingleton(writer ?? TextWriter.Null);
34+
configureTestServices?.Invoke(services);
3235
})
3336
.UseCommandHandler<ExecuteCommand, ExecuteCommandHandler>(),
3437
useDefault: false);
3538

36-
public void Dispose() => Writer.Dispose();
39+
public void Dispose()
40+
{
41+
}
3742
}

0 commit comments

Comments
 (0)