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
2 changes: 1 addition & 1 deletion all.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</PropertyGroup>

<ItemGroup Condition="'$(BuildTestsOnly)' != 'true'">
<ProjectReference Include="**/*.csproj" />
<ProjectReference Include="**/*.csproj" Exclude="**/Yamlify.SourceGenerator/*.csproj" />
</ItemGroup>

<ItemGroup Condition="'$(BuildTestsOnly)' == 'true'">
Expand Down
33 changes: 33 additions & 0 deletions src/Yamlify.SourceGenerator/NullableAttributes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// These attributes are defined here because netstandard2.0 doesn't include them.
// The compiler will use these definitions for nullable flow analysis.

#if NETSTANDARD2_0

namespace System.Diagnostics.CodeAnalysis;

/// <summary>
/// Specifies that when a method returns <see cref="ReturnValue"/>,
/// the parameter will not be null even if the corresponding type allows it.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class NotNullWhenAttribute : Attribute
{
/// <summary>
/// Initializes the attribute with the specified return value condition.
/// </summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value,
/// the associated parameter will not be null.
/// </param>
public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;

/// <summary>
/// Gets the return value condition.
/// </summary>
public bool ReturnValue { get; }
}

#endif
10 changes: 5 additions & 5 deletions src/Yamlify.SourceGenerator/YamlSourceGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -498,15 +499,14 @@ private static void GenerateReadMethod(StringBuilder sb, TypeToGenerate type, IR

// Check if this type is polymorphic (from [YamlSerializable] or [YamlPolymorphic] attributes)
var polyInfo = GetPolymorphicInfoForType(type);
var isPolymorphicBase = polyInfo is not null && polyInfo.DerivedTypes.Count > 0;
if (isPolymorphicBase && (type.Symbol.IsAbstract || type.Symbol.TypeKind == TypeKind.Interface))
if (polyInfo is { DerivedTypes.Count: > 0 } && (type.Symbol.IsAbstract || type.Symbol.TypeKind == TypeKind.Interface))
{
// Generate polymorphic read - dispatch to correct derived type based on discriminator
GeneratePolymorphicRead(sb, type, polyInfo, allTypes, fullTypeName);
sb.AppendLine(" }");
return;
}
else if (isPolymorphicBase)
else if (polyInfo is { DerivedTypes.Count: > 0 })
{
// Non-abstract base type with derived types - still needs polymorphic dispatch
// but must also handle the case where the base type itself is serialized
Expand Down Expand Up @@ -2678,7 +2678,7 @@ private static string ToKebabCase(string name)
return null;
}

private static bool IsListOrArray(ITypeSymbol type, out ITypeSymbol? elementType, out bool isHashSet)
private static bool IsListOrArray(ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? elementType, out bool isHashSet)
{
elementType = null;
isHashSet = false;
Expand Down Expand Up @@ -2714,7 +2714,7 @@ private static bool IsListOrArray(ITypeSymbol type, out ITypeSymbol? elementType
return false;
}

private static bool IsDictionary(ITypeSymbol type, out ITypeSymbol? keyType, out ITypeSymbol? valueType)
private static bool IsDictionary(ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? keyType, [NotNullWhen(true)] out ITypeSymbol? valueType)
{
keyType = null;
valueType = null;
Expand Down
4 changes: 4 additions & 0 deletions src/Yamlify.SourceGenerator/Yamlify.SourceGenerator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>$(WarningsAsErrors);nullable</WarningsAsErrors>
<ImplicitUsings>disable</ImplicitUsings>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>

<!-- Package metadata - not published separately, bundled with Yamlify -->
<IsPackable>false</IsPackable>

<!-- Suppress analyzer release tracking warning (bundled with main package) -->
<NoWarn>$(NoWarn);RS2008</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
76 changes: 62 additions & 14 deletions src/Yamlify/Core/Utf8YamlReader.Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,22 +259,54 @@ private void SkipSpaces()
private void SkipFlowWhitespaceAndComments()
{
bool hadWhitespace = false;
bool atStartOfLine = false; // True when we just crossed a line break
// DON'T reset - preserve the flag if it was set from previous call
// _crossedLineBreakInFlow = false;

while (_consumed < _buffer.Length)
{
byte current = CurrentByte();

if (current == Space || current == Tab)
if (current == Tab)
{
hadWhitespace = true;
// Tabs are only valid in flow context if they're followed by:
// - more whitespace/line break (empty line or tab-only indentation)
// - flow indicators (], }, ,, :)
// Tabs before CONTENT (scalars, [, {, etc.) are invalid
if (atStartOfLine)
{
// Check if this tab is followed by more whitespace/line break, flow end, or content
int peekPos = _consumed + 1;
while (peekPos < _buffer.Length && _buffer[peekPos] == Tab)
{
peekPos++;
}
// If there's non-whitespace content after the tab(s), check if it's a flow indicator
if (peekPos < _buffer.Length && !IsWhitespaceOrLineBreak(_buffer[peekPos]))
{
byte afterTabs = _buffer[peekPos];
// Flow end indicators and comma are OK after tabs
if (afterTabs != SequenceEnd && afterTabs != MappingEndChar &&
afterTabs != CollectEntry && afterTabs != MappingValue)
{
throw new YamlException("Tabs cannot be used at the start of a line in flow context before content", Position);
}
}
}
_consumed++;
}
else if (current == Space)
{
hadWhitespace = true;
atStartOfLine = false; // Spaces clear the "at start of line" state
_consumed++;
}
else if (IsLineBreak(current))
{
hadWhitespace = true;
_crossedLineBreakInFlow = true; // Track that we crossed a line break
atStartOfLine = true; // We're now at the start of a new line
ConsumeLineBreak();
}
else if (current == Comment)
Expand All @@ -287,6 +319,7 @@ private void SkipFlowWhitespaceAndComments()
}
SkipToEndOfLine();
hadWhitespace = false;
atStartOfLine = false;
}
else
{
Expand Down Expand Up @@ -393,14 +426,12 @@ private bool HasTabsInIndentation()
// Check for tabs before content while still in the indentation zone
// Tabs are only invalid if they appear at positions that would affect indentation
int pos = _lineStart;
bool foundNonTabWhitespace = false;

while (pos < _consumed && pos < _buffer.Length)
{
byte b = _buffer[pos];
if (b == Space)
{
foundNonTabWhitespace = true;
pos++;
}
else if (b == Tab)
Expand Down Expand Up @@ -515,10 +546,11 @@ private int DetectBlockScalarIndentation()
}

// Check what comes after the spaces
if (pos < _buffer.Length && !IsWhitespaceOrLineBreak(_buffer[pos]))
if (pos < _buffer.Length && !IsLineBreak(_buffer[pos]))
{
// Found content - this line determines the indentation
// But if previous spaces-only lines had MORE spaces, that's invalid
// Found non-linebreak content - this line determines the indentation.
// Tabs after spaces are CONTENT, not indentation, so this is a content line.
// Check if previous spaces-only lines had MORE spaces - that's invalid.
if (maxSpacesOnlyIndent > 0 && spaces < maxSpacesOnlyIndent)
{
// Content at lower indentation than preceding spaces-only lines
Expand All @@ -528,19 +560,13 @@ private int DetectBlockScalarIndentation()
return spaces;
}

// This is a spaces-only line (or empty line)
// Track the maximum spaces seen
// This is an empty line (spaces followed immediately by line break)
// Track the maximum spaces seen on empty lines
if (spaces > maxSpacesOnlyIndent)
{
maxSpacesOnlyIndent = spaces;
}

// Skip to end of line
while (pos < _buffer.Length && !IsLineBreak(_buffer[pos]))
{
pos++;
}

// Skip the line break
if (pos < _buffer.Length && IsLineBreak(_buffer[pos]))
{
Expand Down Expand Up @@ -601,6 +627,28 @@ private bool MatchesBytes(ReadOnlySpan<byte> expected)
return _buffer.Slice(_consumed, expected.Length).SequenceEqual(expected);
}

/// <summary>
/// Checks if the current position matches a directive name followed by whitespace or end of line.
/// This ensures that %YAML is not confused with %YAMLL (a reserved directive).
/// </summary>
private bool MatchesDirective(ReadOnlySpan<byte> directiveName)
{
if (!MatchesBytes(directiveName))
{
return false;
}

// After the directive name, there must be whitespace or end of buffer
int afterDirective = _consumed + directiveName.Length;
if (afterDirective >= _buffer.Length)
{
return true; // End of input after directive name is OK
}

byte next = _buffer[afterDirective];
return IsWhitespaceOrLineBreak(next);
}

private static bool IsFlowIndicator(byte b) =>
b == SequenceStart || b == SequenceEnd ||
b == MappingStartChar || b == MappingEndChar ||
Expand Down
Loading