Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[.NET] Add initial nullable reference types support #281

Closed
wants to merge 6 commits into from
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
- [JavaScript] update dependency messages up to at least v19.1.4 ([#268](https://github.com/cucumber/gherkin/pull/268))
- [Java] update dependency messages up to v26 ([#269](https://github.com/cucumber/gherkin/pull/269))
- [Ruby] update dependency messages up to v26 ([#267](https://github.com/cucumber/gherkin/pull/267))
- [.NET] Add initial nullable reference types support

### Changed
- [.NET] Drop unsupported frameworks. Now supported target frameworks are .NET 8, .NET Standard 2.0 ([#265](https://github.com/cucumber/gherkin/pull/265))
Expand Down
3 changes: 2 additions & 1 deletion dotnet/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<Project>

<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

</Project>
36 changes: 36 additions & 0 deletions dotnet/Gherkin.Specs/Compatibility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#if NETFRAMEWORK

using System.ComponentModel;

namespace System.Runtime.CompilerServices
{
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit { }

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
internal sealed class RequiredMemberAttribute : Attribute { }

[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
internal sealed class CompilerFeatureRequiredAttribute(string featureName) : Attribute
{
public string FeatureName { get; } = featureName;
public bool IsOptional { get; init; }
public const string RefStructs = nameof(RefStructs);
public const string RequiredMembers = nameof(RequiredMembers);
}
}

namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
internal sealed class SetsRequiredMembersAttribute : Attribute;

[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class NotNullWhenAttribute(bool returnValue) : Attribute
{
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; } = returnValue;
}
}

#endif
2 changes: 1 addition & 1 deletion dotnet/Gherkin.Specs/EventStubs/GherkinEventsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private void AddParseError(List<Envelope> events, ParserException e, String uri)
Message = e.Message,
Source = new SourceReference()
{
Location = new Location(e.Location.Column, e.Location.Line),
Location = new Location(e.Location!.Column, e.Location.Line),
Uri = uri
}
}
Expand Down
10 changes: 5 additions & 5 deletions dotnet/Gherkin.Specs/EventTestBase.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using System.Diagnostics;
using System.Text;
using FluentAssertions;
using Gherkin.CucumberMessages;
using Gherkin.CucumberMessages.Types;
using Gherkin.Specs.EventStubs;
using Gherkin.Specs.Helper;
using System.Diagnostics;
using System.Text;

namespace Gherkin.Specs;

Expand All @@ -29,15 +29,15 @@ protected void AssertEvents(string testFeatureFile, List<Envelope> actualGherkin
$"{testFeatureFile} is not generating the same content as {testFile.ExpectedFileFullPath}");
}

private string NormalizeNewLines(string value)
private string? NormalizeNewLines(string value)
{
return value?.Replace("\r\n", "\n").Replace("\n", Environment.NewLine);
}

protected class TestFile
{
public string FullPath { get; set; }
public string ExpectedFileFullPath { get; set; }
public required string FullPath { get; set; }
public required string ExpectedFileFullPath { get; set; }
}

protected TestFile GetFullPathToTestFeatureFile(string testFeatureFile, string category, string filePostfix)
Expand Down
2 changes: 1 addition & 1 deletion dotnet/Gherkin.Specs/Helper/NDJsonParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static List<T> Deserialize<T>(string ndjson)
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
}
});
result.Add(deserializedObject);
result.Add(deserializedObject!);
}

return result;
Expand Down
4 changes: 2 additions & 2 deletions dotnet/Gherkin/Ast/DocString.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
namespace Gherkin.Ast;

public class DocString(Location location, string contentType, string content, string delimiter = null) : StepArgument, IHasLocation
public class DocString(Location location, string contentType, string content, string? delimiter = null) : StepArgument, IHasLocation
{
public Location Location { get; } = location;
public string ContentType { get; } = contentType;
public string Content { get; } = content;
public string Delimiter { get; } = delimiter;
public string? Delimiter { get; } = delimiter;
}
2 changes: 2 additions & 0 deletions dotnet/Gherkin/AstBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Gherkin;

#nullable disable

public class AstBuilder<T> : IAstBuilder<T>
{
private readonly Stack<AstNode> _stack = new();
Expand Down
18 changes: 8 additions & 10 deletions dotnet/Gherkin/AstNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public class AstNode(RuleType ruleType)

public RuleType RuleType { get; } = ruleType;

public Token GetToken(TokenType tokenType)
public Token? GetToken(TokenType tokenType)
{
return GetSingle<Token>((RuleType)tokenType);
}
Expand All @@ -16,38 +16,36 @@ public IEnumerable<Token> GetTokens(TokenType tokenType)
return GetItems<Token>((RuleType)tokenType);
}

public T GetSingle<T>(RuleType ruleType)
public T? GetSingle<T>(RuleType ruleType) where T : notnull
{
return GetItems<T>(ruleType).SingleOrDefault();
}

public IEnumerable<T> GetItems<T>(RuleType ruleType)
{
IList<object> items;
if (!subItems.TryGetValue(ruleType, out items))
if (!subItems.TryGetValue(ruleType, out var items))
{
return Enumerable.Empty<T>();
}
return items.Cast<T>();
}

public void SetSingle<T>(RuleType ruleType, T value)
public void SetSingle<T>(RuleType ruleType, T value) where T : notnull
{
subItems[ruleType] = new object[] { value };
subItems[ruleType] = [value];
}

public void AddRange<T>(RuleType ruleType, IEnumerable<T> values)
public void AddRange<T>(RuleType ruleType, IEnumerable<T> values) where T : notnull
{
foreach (var value in values)
{
Add(ruleType, value);
}
}

public void Add<T>(RuleType ruleType, T obj)
public void Add<T>(RuleType ruleType, T obj) where T : notnull
{
IList<object> items;
if (!subItems.TryGetValue(ruleType, out items))
if (!subItems.TryGetValue(ruleType, out var items))
{
items = new List<object>();
subItems.Add(ruleType, items);
Expand Down
36 changes: 36 additions & 0 deletions dotnet/Gherkin/Compatibility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#if NETSTANDARD2_0

using System.ComponentModel;

namespace System.Runtime.CompilerServices
{
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit { }

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
internal sealed class RequiredMemberAttribute : Attribute { }

[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
internal sealed class CompilerFeatureRequiredAttribute(string featureName) : Attribute
{
public string FeatureName { get; } = featureName;
public bool IsOptional { get; init; }
public const string RefStructs = nameof(RefStructs);
public const string RequiredMembers = nameof(RequiredMembers);
}
}

namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
internal sealed class SetsRequiredMembersAttribute : Attribute;

[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class NotNullWhenAttribute(bool returnValue) : Attribute
{
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; } = returnValue;
}
}

#endif
24 changes: 12 additions & 12 deletions dotnet/Gherkin/CucumberMessages/AstMessagesConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ private IReadOnlyCollection<Comment> ConvertComments(Ast.GherkinDocument gherkin
}).ToReadOnlyCollection();
}

private Feature ConvertFeature(Ast.GherkinDocument gherkinDocument)
private Feature? ConvertFeature(Ast.GherkinDocument gherkinDocument)
{
var feature = gherkinDocument.Feature;
if (feature == null)
Expand Down Expand Up @@ -79,13 +79,13 @@ private RuleChild ConvertToRuleChild(IHasLocation hasLocation)
return new RuleChild(tuple.Item1, tuple.Item3);
}

private Tuple<Background, Rule, Scenario> ConvertToChild(IHasLocation hasLocation)
private Tuple<Background?, Rule?, Scenario?> ConvertToChild(IHasLocation hasLocation)
{
switch (hasLocation)
{
case Gherkin.Ast.Background background:
case Ast.Background background:
var backgroundSteps = background.Steps.Select(ConvertStep).ToList();
return new Tuple<Background, Rule, Scenario>(new Background
return new Tuple<Background?, Rule?, Scenario?>(new Background
{
Id = idGenerator.GetNewId(),
Location = ConvertLocation(background.Location),
Expand All @@ -98,7 +98,7 @@ private Tuple<Background, Rule, Scenario> ConvertToChild(IHasLocation hasLocatio
var steps = scenario.Steps.Select(ConvertStep).ToList();
var examples = scenario.Examples.Select(ConvertExamples).ToReadOnlyCollection();
var tags = scenario.Tags.Select(ConvertTag).ToReadOnlyCollection();
return new Tuple<Background, Rule, Scenario>(null, null, new Scenario()
return new Tuple<Background?, Rule?, Scenario?>(null, null, new Scenario()
{
Id = idGenerator.GetNewId(),
Keyword = scenario.Keyword,
Expand All @@ -113,7 +113,7 @@ private Tuple<Background, Rule, Scenario> ConvertToChild(IHasLocation hasLocatio
{
var ruleChildren = rule.Children.Select(ConvertToRuleChild).ToReadOnlyCollection();
var ruleTags = rule.Tags.Select(ConvertTag).ToReadOnlyCollection();
return new Tuple<Background, Rule, Scenario>(null, new Rule
return new Tuple<Background?, Rule?, Scenario?>(null, new Rule
{
Id = idGenerator.GetNewId(),
Name = CucumberMessagesDefaults.UseDefault(rule.Name, CucumberMessagesDefaults.DefaultName),
Expand Down Expand Up @@ -159,7 +159,7 @@ private IReadOnlyCollection<TableRow> ConvertToTableBody(Ast.Examples examples)
return ConvertToTableRow(examples.TableBody);
}

private IReadOnlyCollection<TableRow> ConvertToTableRow(IEnumerable<Gherkin.Ast.TableRow> rows)
private IReadOnlyCollection<TableRow> ConvertToTableRow(IEnumerable<Ast.TableRow> rows)
{
return rows.Select(b =>
new TableRow
Expand All @@ -170,7 +170,7 @@ private IReadOnlyCollection<TableRow> ConvertToTableRow(IEnumerable<Gherkin.Ast.
}).ToReadOnlyCollection();
}

private TableRow ConvertTableHeader(Ast.Examples examples)
private TableRow? ConvertTableHeader(Ast.Examples examples)
{
if (examples.TableHeader == null)
return null;
Expand Down Expand Up @@ -204,8 +204,8 @@ private TableCell ConvertCell(Ast.TableCell c)

private Step ConvertStep(Ast.Step step)
{
DataTable dataTable = null;
if (step.Argument is Gherkin.Ast.DataTable astDataTable)
DataTable? dataTable = null;
if (step.Argument is Ast.DataTable astDataTable)
{
var rows = ConvertToTableRow(astDataTable.Rows);
dataTable = new DataTable
Expand All @@ -215,8 +215,8 @@ private Step ConvertStep(Ast.Step step)
};
}

DocString docString = null;
if (step.Argument is Gherkin.Ast.DocString astDocString)
DocString? docString = null;
if (step.Argument is Ast.DocString astDocString)
{
docString = new DocString
{
Expand Down
31 changes: 14 additions & 17 deletions dotnet/Gherkin/CucumberMessages/Pickles/PickleCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ protected virtual void CompileScenarioOutline(List<Pickle> pickles,
}
}

protected virtual PickleStep CreatePickleStep(Step step, string text, PickleStepArgument argument, IEnumerable<string> astNodeIds)
protected virtual PickleStep CreatePickleStep(Step step, string text, PickleStepArgument? argument, IEnumerable<string> astNodeIds)
{
CurrentStepKeywordType = GetKeywordType(step, CurrentStepKeywordType);
return new PickleStep(argument, astNodeIds, idGenerator.GetNewId(), text, CurrentStepKeywordType);
Expand All @@ -174,31 +174,28 @@ protected virtual PickleStep CreatePickleStep(Step step, string text, PickleStep
protected virtual StepKeywordType GetKeywordType(Step step, StepKeywordType lastStepKeywordType)
{
var stepKeywordType = step.KeywordType;
switch (stepKeywordType)

return step.KeywordType switch
{
case StepKeywordType.Context:
case StepKeywordType.Action:
case StepKeywordType.Outcome:
case StepKeywordType.Unknown:
return stepKeywordType;
case StepKeywordType.Conjunction:
return lastStepKeywordType == StepKeywordType.Unspecified ||
lastStepKeywordType == StepKeywordType.Unknown
? StepKeywordType.Context
: lastStepKeywordType;
default:
return StepKeywordType.Unspecified;
}
StepKeywordType.Context => StepKeywordType.Context,
StepKeywordType.Action => StepKeywordType.Action,
StepKeywordType.Outcome => StepKeywordType.Outcome,
StepKeywordType.Unknown => StepKeywordType.Unknown,
StepKeywordType.Conjunction => lastStepKeywordType is StepKeywordType.Unspecified or StepKeywordType.Unknown
? StepKeywordType.Context
: lastStepKeywordType,
_ => StepKeywordType.Unspecified
};
}


protected virtual PickleStepArgument CreatePickleArgument(Step argument)
protected virtual PickleStepArgument? CreatePickleArgument(Step argument)
Romfos marked this conversation as resolved.
Show resolved Hide resolved
{
var noCells = Enumerable.Empty<TableCell>();
return CreatePickleArgument(argument, noCells, noCells);
}

protected virtual PickleStepArgument CreatePickleArgument(Step step, IEnumerable<TableCell> variableCells, IEnumerable<TableCell> valueCells)
protected virtual PickleStepArgument? CreatePickleArgument(Step step, IEnumerable<TableCell> variableCells, IEnumerable<TableCell> valueCells)
{
if (step.DataTable != null)
{
Expand Down
12 changes: 6 additions & 6 deletions dotnet/Gherkin/CucumberMessages/Types/Background.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ namespace Gherkin.CucumberMessages.Types;
public class Background
{
[DataMember(Name = "location")]
public Location Location { get; set; }
public required Location Location { get; set; }

[DataMember(Name = "keyword")]
public string Keyword { get; set; }
public required string Keyword { get; set; }

[DataMember(Name = "name")]
public string Name { get; set; }
public required string Name { get; set; }

[DataMember(Name = "description")]
public string Description { get; set; }
public required string Description { get; set; }

[DataMember(Name = "steps")]
public IReadOnlyCollection<Step> Steps { get; set; }
public required IReadOnlyCollection<Step> Steps { get; set; }

[DataMember(Name = "id")]
public string Id { get; set; }
public required string Id { get; set; }
}
4 changes: 2 additions & 2 deletions dotnet/Gherkin/CucumberMessages/Types/Comment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ namespace Gherkin.CucumberMessages.Types;
public class Comment
{
[DataMember(Name = "location")]
public Location Location { get; set; }
public required Location Location { get; set; }

[DataMember(Name = "text")]
public string Text { get; set; }
public required string Text { get; set; }
}
Loading