diff --git a/src/SourceGenerators.Tests/CommentPropertyExtensionsTests.cs b/src/SourceGenerators.Tests/CommentPropertyExtensionsTests.cs new file mode 100644 index 0000000..961a3dd --- /dev/null +++ b/src/SourceGenerators.Tests/CommentPropertyExtensionsTests.cs @@ -0,0 +1,101 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using UnionTypes.Toolkit.Generators; + +namespace Tests; + +[TestClass] +public class CommentPropertyExtensionsTests +{ + [TestMethod] + public void TryGetCommentProperty_BareProperty_ReturnsTrueValue() + { + var declaration = GetClassDeclaration("// @union\nclass Example { }"); + + var found = declaration.TryGetCommentProperty("union", out var value); + + Assert.IsTrue(found); + Assert.AreEqual("true", value); + Assert.IsTrue(declaration.HasCommentProperty("union")); + } + + [TestMethod] + public void TryGetCommentProperty_ValueStopsAtCommentDelimiters() + { + var declaration = GetClassDeclaration("/* @name=example, @enabled */ class Example { }"); + + var found = declaration.TryGetCommentProperty("name", out var value); + + Assert.IsTrue(found); + Assert.AreEqual("example", value); + Assert.IsTrue(declaration.HasCommentProperty("enabled")); + } + + [TestMethod] + public void TryGetCommentProperty_MissingProperty_ReturnsFalse() + { + var declaration = GetClassDeclaration("// an unrelated comment\nclass Example { }"); + + var found = declaration.TryGetCommentProperty("union", out var value); + + Assert.IsFalse(found); + Assert.IsNull(value); + Assert.IsFalse(declaration.HasCommentProperty("union")); + } + + [TestMethod] + public void TryGetCommentProperty_Generic_ConvertsValueAndRejectsInvalidValue() + { + var declaration = GetClassDeclaration("// @count=42 @enabled\nclass Example { }"); + + var countFound = declaration.TryGetCommentProperty("count", out var count); + var enabledFound = declaration.TryGetCommentProperty("enabled", out var enabled); + var invalidFound = declaration.TryGetCommentProperty("enabled", out var invalid); + + Assert.IsTrue(countFound); + Assert.AreEqual(42, count); + Assert.IsTrue(enabledFound); + Assert.IsTrue(enabled); + Assert.IsFalse(invalidFound); + Assert.AreEqual(0, invalid); + } + + [TestMethod] + public void TryGetCommentProperty_Symbol_SearchesAllDeclarationNodes() + { + var compilation = TestHelpers.CreateCompilation( + "partial class Example { }", + "// @union\npartial class Example { }"); + var symbol = compilation.GetTypeByMetadataName("Example"); + + Assert.IsNotNull(symbol); + var found = symbol!.TryGetCommentProperty("union", out var value); + + Assert.IsTrue(found); + Assert.AreEqual("true", value); + Assert.IsTrue(symbol.HasCommentProperty("union")); + } + + [TestMethod] + public void TryGetCommentProperty_GenericSymbol_ConvertsDeclarationValue() + { + var compilation = TestHelpers.CreateCompilation("// @priority=7\nclass Example { }"); + var symbol = compilation.GetTypeByMetadataName("Example"); + + Assert.IsNotNull(symbol); + var found = symbol!.TryGetCommentProperty("priority", out var priority); + + Assert.IsTrue(found); + Assert.AreEqual(7, priority); + } + + private static ClassDeclarationSyntax GetClassDeclaration(string source) + { + return CSharpSyntaxTree.ParseText(source) + .GetRoot() + .DescendantNodes() + .OfType() + .Single(); + } +} \ No newline at end of file diff --git a/src/SourceGenerators/CommentProperties.cs b/src/SourceGenerators/CommentPropertyExtensions.cs similarity index 64% rename from src/SourceGenerators/CommentProperties.cs rename to src/SourceGenerators/CommentPropertyExtensions.cs index cd5b71a..6c6ba51 100644 --- a/src/SourceGenerators/CommentProperties.cs +++ b/src/SourceGenerators/CommentPropertyExtensions.cs @@ -1,12 +1,7 @@ using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace UnionTypes.Toolkit.Generators; @@ -34,36 +29,48 @@ public static bool HasCommentProperty(this ISymbol symbol, string propertyName) public static bool TryGetCommentProperty(this SyntaxNode node, string propertyName, out string? value) { value = null; - var commentTrivia = node.GetLeadingTrivia().Where(t => t.IsKind(SyntaxKind.SingleLineCommentTrivia) || t.IsKind(SyntaxKind.MultiLineCommentTrivia)).ToArray(); - foreach (var trivia in commentTrivia) + + foreach (var trivia in node.GetLeadingTrivia()) { - var text = trivia.ToString(); - var prefix = "@" + propertyName; - var startIndex = text.IndexOf(prefix); - if (startIndex >= 0) - { - var endOfPrefix = startIndex + prefix.Length; - - if (endOfPrefix < text.Length && text[endOfPrefix] == '=') + if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) + || trivia.IsKind(SyntaxKind.MultiLineCommentTrivia)) + { + var text = trivia.ToString(); + var prefix = "@" + propertyName; + var startIndex = text.IndexOf(prefix); + if (startIndex >= 0) { - startIndex = endOfPrefix + 1; - var endIndex = text.IndexOfAny(new[] { ' ', '\t', '\r', '\n' }, startIndex); - if (endIndex < 0) - endIndex = text.Length; - value = text.Substring(startIndex, endIndex - startIndex); - return true; - } - else if (endOfPrefix == text.Length - || text.IndexOfAny(new[] { ' ', '\t', '\r', '\n' }, endOfPrefix) >= endOfPrefix) - { - value = "true"; - return true; + var endOfPrefix = startIndex + prefix.Length; + + if (endOfPrefix < text.Length && text[endOfPrefix] == '=') + { + // determine value after = + startIndex = endOfPrefix + 1; + var endIndex = text.IndexOfAny(_commentPropertyEndingTokens, startIndex); + if (endIndex < 0) + endIndex = text.Length; + value = text.Substring(startIndex, endIndex - startIndex); + return true; + } + else if (endOfPrefix == text.Length + || text.IndexOfAny(_commentPropertyEndingTokens, endOfPrefix) >= endOfPrefix) + { + // property without a value is considered to be "true" + value = "true"; + return true; + } } - } + } } + return false; } + /// + /// Any of these characters denotes the end of a comment property + /// + private static readonly char[] _commentPropertyEndingTokens = new[] { ' ', '\t', '\r', '\n', ',', ';', '|', ':', '(', ')', '[', ']', '{', '}' }; + /// /// Returns true if the comment property exists in the node's leading trivia, and outputs properties assigned value if present and convertible to the type T. ///