From 3db084b941686863384bf63a3514cebb56a6bd3e Mon Sep 17 00:00:00 2001 From: Alpha_Centauri Date: Tue, 20 May 2025 16:14:44 +0200 Subject: [PATCH 1/9] new attribute class for duco --- .../RobotKeywordAttributes.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/RobotFramework.DotNetLibraryBase/RobotKeywordAttributes.cs diff --git a/src/RobotFramework.DotNetLibraryBase/RobotKeywordAttributes.cs b/src/RobotFramework.DotNetLibraryBase/RobotKeywordAttributes.cs new file mode 100644 index 0000000..43b33e1 --- /dev/null +++ b/src/RobotFramework.DotNetLibraryBase/RobotKeywordAttributes.cs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2024 Daniel Biehl +// +// SPDX-License-Identifier: Apache-2.0 + +namespace RobotFramework.DotNetLibraryBase; + +using System; + +[AttributeUsage(AttributeTargets.Method)] +#pragma warning disable 1591 +public class RobotKeywordDocumentationAttribute : Attribute +{ + public string Documentation { get; } + + public RobotKeywordDocumentationAttribute(string documentation) + { + Documentation = documentation; + } +} \ No newline at end of file From d25f0ba2a38be9cdf99ad0fc11b2e0de65de8487 Mon Sep 17 00:00:00 2001 From: Alpha_Centauri Date: Tue, 20 May 2025 16:17:20 +0200 Subject: [PATCH 2/9] new class to get xml docu --- .../ReflectionExtensions.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/RobotFramework.DotNetLibraryBase/ReflectionExtensions.cs diff --git a/src/RobotFramework.DotNetLibraryBase/ReflectionExtensions.cs b/src/RobotFramework.DotNetLibraryBase/ReflectionExtensions.cs new file mode 100644 index 0000000..fd51ecb --- /dev/null +++ b/src/RobotFramework.DotNetLibraryBase/ReflectionExtensions.cs @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2024 Daniel Biehl +// +// SPDX-License-Identifier: Apache-2.0 + +namespace RobotFramework.DotNetLibraryBase; + +using System.IO; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; +using System.Text; + +#pragma warning disable 1591 +public static class ReflectionExtensions +{ + public static string? GetXmlDocumentation(this MethodInfo methodInfo) + { + try + { + // Get the assembly that contains the method + var assembly = methodInfo.DeclaringType?.Assembly; + if (assembly == null) return null; + + // Get the XML documentation file path + var xmlPath = Path.ChangeExtension(assembly.Location, "xml"); + if (!File.Exists(xmlPath)) return null; + + // Load and parse the XML documentation + var doc = XDocument.Load(xmlPath); + + // Build the member ID that matches the XML documentation format + var memberName = $"M:{methodInfo.DeclaringType?.FullName}.{methodInfo.Name}"; + + // Find the member documentation + var member = doc.Root?.Elements("members") + .Elements("member") + .FirstOrDefault(m => m.Attribute("name")?.Value == memberName); + + if (member == null) return null; + + // Build complete documentation including summary, params, and returns + var documentation = new StringBuilder(); + + // Add summary + var summary = member.Element("summary"); + if (summary != null) + { + documentation.AppendLine(summary.Value.Trim()); + documentation.AppendLine(); + } + + // Add parameters + var parameters = member.Elements("param"); + if (parameters.Any()) + { + documentation.AppendLine("Parameters:"); + foreach (var param in parameters) + { + var name = param.Attribute("name")?.Value; + var description = param.Value.Trim(); + documentation.AppendLine($"- {name}: {description}"); + } + documentation.AppendLine(); + } + + // Add return value + var returns = member.Element("returns"); + if (returns != null) + { + documentation.AppendLine("Returns:"); + documentation.AppendLine(returns.Value.Trim()); + } + + return documentation.ToString().Trim(); + } + catch + { + // If anything goes wrong, return null + return null; + } + } +} From 3a71a73dc5c43b4a9f346368cf88e638a3ac2f0a Mon Sep 17 00:00:00 2001 From: Alpha_Centauri Date: Tue, 20 May 2025 16:21:05 +0200 Subject: [PATCH 3/9] implement _documentation var to get docu from custom att --- .../LibraryInfo.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/RobotFramework.DotNetLibraryBase/LibraryInfo.cs b/src/RobotFramework.DotNetLibraryBase/LibraryInfo.cs index 5c00bf3..a849e2d 100644 --- a/src/RobotFramework.DotNetLibraryBase/LibraryInfo.cs +++ b/src/RobotFramework.DotNetLibraryBase/LibraryInfo.cs @@ -8,6 +8,9 @@ namespace RobotFramework.DotNetLibraryBase; using System.Collections; using System.Collections.Generic; using System.Reflection; +using System.Linq; +using System.Xml; +#pragma warning disable 1591 internal enum RobotLibraryConstants { @@ -44,7 +47,23 @@ public KeywordInfo(IEnumerable methods) if (Methods.Length == 0) throw new ArgumentException("At least one method is required", nameof(methods)); - _documentation = new Lazy(() => null); // TODO: Get doc from attribute + _documentation = new Lazy(() => { + // Try to get documentation from custom attribute first + var docAttribute = Methods[0].GetCustomAttribute(); + var attributeDoc = docAttribute?.Documentation; + + // Get XML documentation + var xmlDoc = Methods[0].GetXmlDocumentation(); + + // If we have both, combine them + if (!string.IsNullOrEmpty(attributeDoc) && !string.IsNullOrEmpty(xmlDoc)) + { + return $"{xmlDoc}\n\nAdditional Notes:\n{attributeDoc}"; + } + + // Return whichever one is available + return attributeDoc ?? xmlDoc; + }); _tags = new Lazy(() => Array.Empty()); // TODO: Get tags from attribute _arguments = new Lazy(CollectArguments); } From 560e9d26e783c2f342da7f4b01bf6af4d21907e8 Mon Sep 17 00:00:00 2001 From: Alpha_Centauri Date: Tue, 20 May 2025 16:22:44 +0200 Subject: [PATCH 4/9] active GenerateDocumentationfile --- .../RobotFramework.DotNetLibraryBase.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/RobotFramework.DotNetLibraryBase/RobotFramework.DotNetLibraryBase.csproj b/src/RobotFramework.DotNetLibraryBase/RobotFramework.DotNetLibraryBase.csproj index c95276c..0a12ed8 100644 --- a/src/RobotFramework.DotNetLibraryBase/RobotFramework.DotNetLibraryBase.csproj +++ b/src/RobotFramework.DotNetLibraryBase/RobotFramework.DotNetLibraryBase.csproj @@ -5,6 +5,7 @@ enable enable 10 + true From da2e4f4f464e5f9deacaeeefd60a218ac38e36e1 Mon Sep 17 00:00:00 2001 From: Alpha_Centauri Date: Tue, 20 May 2025 16:24:10 +0200 Subject: [PATCH 5/9] add func in python side to get keywords docu --- src/DotNetLibraryBase/base.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/DotNetLibraryBase/base.py b/src/DotNetLibraryBase/base.py index 776799f..e281cbc 100644 --- a/src/DotNetLibraryBase/base.py +++ b/src/DotNetLibraryBase/base.py @@ -263,6 +263,17 @@ def get_keyword_types(self, name: str) -> Optional[Mapping[str, Any]]: return {i.Name: [self._convert_type(t) for t in i.Types] for i in keyword_info.Arguments} + def get_keyword_documentation(self, name: str) -> Optional[str]: + """Get documentation for the given keyword.""" + if self._library_info is None: + return None + try: + keyword_info = self._library_info.Keywords[name] + doc = keyword_info.Documentation + return str(doc) if doc is not None else None + except Exception: + return None + def run_keyword(self, name: str, args: Sequence[Any], kwargs: Mapping[str, Any]) -> Any: method = getattr(self._instance, name) real_args = list(args) From 16324cd62a48e977daf7a6f3dc9aa0812b08a295 Mon Sep 17 00:00:00 2001 From: Alpha_Centauri Date: Tue, 20 May 2025 16:26:54 +0200 Subject: [PATCH 6/9] add new cs class for docu in tests --- .../DotNetDemoLibrary/DocumentationExample.cs | 63 +++++++++++++++++++ .../DotNetDemoLibrary.csproj | 5 ++ 2 files changed, 68 insertions(+) create mode 100644 tests/DotNetDemoLibrary/DocumentationExample.cs diff --git a/tests/DotNetDemoLibrary/DocumentationExample.cs b/tests/DotNetDemoLibrary/DocumentationExample.cs new file mode 100644 index 0000000..69e1479 --- /dev/null +++ b/tests/DotNetDemoLibrary/DocumentationExample.cs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Daniel Biehl +// +// SPDX-License-Identifier: Apache-2.0 + +namespace DotNetDemoLibrary; + +using System; +using RobotFramework.DotNetLibraryBase; + +/// +/// Provides examples of using this class to see the docu +/// +public class DocumentationExample +{ + /// + /// This is an example of XML documentation. + /// The documentation will be shown in Robot Framework. + /// + /// + /// This keyword demonstrates XML documentation in IntelliSense. + /// + public void XmlDocumentedKeyword() + { + Console.WriteLine("This keyword uses XML documentation"); + } + + /// + /// Example with parameters + /// + /// The text to display + /// Number of times to repeat + [RobotKeywordDocumentation("This is an example of attribute-based documentation")] + public void AttributeDocumentedKeyword(string text, int count = 1) + { + for (int i = 0; i < count; i++) + { + Console.WriteLine(text); + } + } + + /// + /// This keyword has both XML documentation... + /// + /// Optional message to display + /// The message that was displayed + [RobotKeywordDocumentation("...and attribute documentation (this one will take precedence)")] + public string BothDocumentationTypesKeyword(string message = "Default message") + { + Console.WriteLine(message); + return message; + } + + /// + /// No Docu keyword + /// + /// + /// + /// + public int adding(int a, int b) + { + return a + b; + } +} diff --git a/tests/DotNetDemoLibrary/DotNetDemoLibrary.csproj b/tests/DotNetDemoLibrary/DotNetDemoLibrary.csproj index 8a7681a..0950eed 100644 --- a/tests/DotNetDemoLibrary/DotNetDemoLibrary.csproj +++ b/tests/DotNetDemoLibrary/DotNetDemoLibrary.csproj @@ -5,6 +5,11 @@ enable enable 0.0.1 + true + + + + \ No newline at end of file From 5884419b0313cf63127efc3019a817d83ec220b8 Mon Sep 17 00:00:00 2001 From: Alpha_Centauri Date: Tue, 20 May 2025 16:27:50 +0200 Subject: [PATCH 7/9] create a new robot test --- tests/docu_test.robot | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/docu_test.robot diff --git a/tests/docu_test.robot b/tests/docu_test.robot new file mode 100644 index 0000000..269b3da --- /dev/null +++ b/tests/docu_test.robot @@ -0,0 +1,12 @@ +*** Settings *** +Library DotNetLibraryBase DotNetDemoLibrary.DocumentationExample, DotNetDemoLibrary + +*** Test Cases *** +Test Documentation Types + XML Documented Keyword + Attribute Documented Keyword Hello from Robot count=3 + ${message}= Both Documentation Types Keyword Custom message + Log ${message} + ${result}= Adding 5 5 + Log ${result} + \ No newline at end of file From 7fa5a6fc126fe5291785d84bea62b3d53b662024 Mon Sep 17 00:00:00 2001 From: Alpha_Centauri Date: Wed, 21 May 2025 13:30:02 +0200 Subject: [PATCH 8/9] only get duco inside summary tag --- .../LibraryInfo.cs | 16 ++---- .../ReflectionExtensions.cs | 57 +++++++------------ .../DotNetDemoLibrary/DocumentationExample.cs | 3 - 3 files changed, 26 insertions(+), 50 deletions(-) diff --git a/src/RobotFramework.DotNetLibraryBase/LibraryInfo.cs b/src/RobotFramework.DotNetLibraryBase/LibraryInfo.cs index a849e2d..3566138 100644 --- a/src/RobotFramework.DotNetLibraryBase/LibraryInfo.cs +++ b/src/RobotFramework.DotNetLibraryBase/LibraryInfo.cs @@ -48,21 +48,17 @@ public KeywordInfo(IEnumerable methods) throw new ArgumentException("At least one method is required", nameof(methods)); _documentation = new Lazy(() => { - // Try to get documentation from custom attribute first - var docAttribute = Methods[0].GetCustomAttribute(); - var attributeDoc = docAttribute?.Documentation; - - // Get XML documentation + // Get XML documentation first var xmlDoc = Methods[0].GetXmlDocumentation(); - // If we have both, combine them - if (!string.IsNullOrEmpty(attributeDoc) && !string.IsNullOrEmpty(xmlDoc)) + // Only if XML doc is not available, try attribute + if (string.IsNullOrEmpty(xmlDoc)) { - return $"{xmlDoc}\n\nAdditional Notes:\n{attributeDoc}"; + var docAttribute = Methods[0].GetCustomAttribute(); + return docAttribute?.Documentation; } - // Return whichever one is available - return attributeDoc ?? xmlDoc; + return xmlDoc; }); _tags = new Lazy(() => Array.Empty()); // TODO: Get tags from attribute _arguments = new Lazy(CollectArguments); diff --git a/src/RobotFramework.DotNetLibraryBase/ReflectionExtensions.cs b/src/RobotFramework.DotNetLibraryBase/ReflectionExtensions.cs index fd51ecb..1f28a0d 100644 --- a/src/RobotFramework.DotNetLibraryBase/ReflectionExtensions.cs +++ b/src/RobotFramework.DotNetLibraryBase/ReflectionExtensions.cs @@ -4,6 +4,7 @@ namespace RobotFramework.DotNetLibraryBase; +using System; using System.IO; using System.Linq; using System.Reflection; @@ -27,51 +28,33 @@ public static class ReflectionExtensions // Load and parse the XML documentation var doc = XDocument.Load(xmlPath); - + // Build the member ID that matches the XML documentation format - var memberName = $"M:{methodInfo.DeclaringType?.FullName}.{methodInfo.Name}"; - + var parameters = methodInfo.GetParameters(); + var parameterTypes = parameters.Length == 0 + ? string.Empty + : $"({string.Join(",", parameters.Select(p => p.ParameterType.FullName))})"; + + var memberName = $"M:{methodInfo.DeclaringType?.FullName}.{methodInfo.Name}{parameterTypes}"; + // Find the member documentation var member = doc.Root?.Elements("members") .Elements("member") .FirstOrDefault(m => m.Attribute("name")?.Value == memberName); - if (member == null) return null; - - // Build complete documentation including summary, params, and returns - var documentation = new StringBuilder(); - - // Add summary - var summary = member.Element("summary"); - if (summary != null) + if (member == null) { - documentation.AppendLine(summary.Value.Trim()); - documentation.AppendLine(); - } - - // Add parameters - var parameters = member.Elements("param"); - if (parameters.Any()) - { - documentation.AppendLine("Parameters:"); - foreach (var param in parameters) - { - var name = param.Attribute("name")?.Value; - var description = param.Value.Trim(); - documentation.AppendLine($"- {name}: {description}"); - } - documentation.AppendLine(); - } - - // Add return value - var returns = member.Element("returns"); - if (returns != null) - { - documentation.AppendLine("Returns:"); - documentation.AppendLine(returns.Value.Trim()); + // Try without parameter types if not found (for backward compatibility) + memberName = $"M:{methodInfo.DeclaringType?.FullName}.{methodInfo.Name}"; + member = doc.Root?.Elements("members") + .Elements("member") + .FirstOrDefault(m => m.Attribute("name")?.Value.StartsWith(memberName) == true); + + if (member == null) return null; } - return documentation.ToString().Trim(); + // Return just the summary content + return member.Element("summary")?.Value.Trim(); } catch { @@ -79,4 +62,4 @@ public static class ReflectionExtensions return null; } } -} +} \ No newline at end of file diff --git a/tests/DotNetDemoLibrary/DocumentationExample.cs b/tests/DotNetDemoLibrary/DocumentationExample.cs index 69e1479..b956d08 100644 --- a/tests/DotNetDemoLibrary/DocumentationExample.cs +++ b/tests/DotNetDemoLibrary/DocumentationExample.cs @@ -5,7 +5,6 @@ namespace DotNetDemoLibrary; using System; -using RobotFramework.DotNetLibraryBase; /// /// Provides examples of using this class to see the docu @@ -29,7 +28,6 @@ public void XmlDocumentedKeyword() /// /// The text to display /// Number of times to repeat - [RobotKeywordDocumentation("This is an example of attribute-based documentation")] public void AttributeDocumentedKeyword(string text, int count = 1) { for (int i = 0; i < count; i++) @@ -43,7 +41,6 @@ public void AttributeDocumentedKeyword(string text, int count = 1) /// /// Optional message to display /// The message that was displayed - [RobotKeywordDocumentation("...and attribute documentation (this one will take precedence)")] public string BothDocumentationTypesKeyword(string message = "Default message") { Console.WriteLine(message); From cf7590ec0c03e1e041cdd0f7b250883e313e9186 Mon Sep 17 00:00:00 2001 From: Alpha_Centauri Date: Wed, 21 May 2025 16:46:12 +0200 Subject: [PATCH 9/9] edit reflection class for other generic types --- .../ReflectionExtensions.cs | 90 ++++++++++++++----- 1 file changed, 68 insertions(+), 22 deletions(-) diff --git a/src/RobotFramework.DotNetLibraryBase/ReflectionExtensions.cs b/src/RobotFramework.DotNetLibraryBase/ReflectionExtensions.cs index 1f28a0d..d3bb9f0 100644 --- a/src/RobotFramework.DotNetLibraryBase/ReflectionExtensions.cs +++ b/src/RobotFramework.DotNetLibraryBase/ReflectionExtensions.cs @@ -11,7 +11,6 @@ namespace RobotFramework.DotNetLibraryBase; using System.Xml.Linq; using System.Text; -#pragma warning disable 1591 public static class ReflectionExtensions { public static string? GetXmlDocumentation(this MethodInfo methodInfo) @@ -29,29 +28,13 @@ public static class ReflectionExtensions // Load and parse the XML documentation var doc = XDocument.Load(xmlPath); - // Build the member ID that matches the XML documentation format - var parameters = methodInfo.GetParameters(); - var parameterTypes = parameters.Length == 0 - ? string.Empty - : $"({string.Join(",", parameters.Select(p => p.ParameterType.FullName))})"; + // Try to find the member with exact match first + var member = TryFindMember(doc, methodInfo, exact: true); - var memberName = $"M:{methodInfo.DeclaringType?.FullName}.{methodInfo.Name}{parameterTypes}"; + // If not found, try a more lenient search + member ??= TryFindMember(doc, methodInfo, exact: false); - // Find the member documentation - var member = doc.Root?.Elements("members") - .Elements("member") - .FirstOrDefault(m => m.Attribute("name")?.Value == memberName); - - if (member == null) - { - // Try without parameter types if not found (for backward compatibility) - memberName = $"M:{methodInfo.DeclaringType?.FullName}.{methodInfo.Name}"; - member = doc.Root?.Elements("members") - .Elements("member") - .FirstOrDefault(m => m.Attribute("name")?.Value.StartsWith(memberName) == true); - - if (member == null) return null; - } + if (member == null) return null; // Return just the summary content return member.Element("summary")?.Value.Trim(); @@ -62,4 +45,67 @@ public static class ReflectionExtensions return null; } } + + private static XElement? TryFindMember(XDocument doc, MethodInfo methodInfo, bool exact) + { + var typeName = methodInfo.DeclaringType?.FullName ?? string.Empty; + var methodName = methodInfo.Name; + + // Get all member elements + var members = doc.Root?.Elements("members") + .Elements("member") + .Where(m => m.Attribute("name")?.Value.StartsWith($"M:{typeName}.{methodName}") == true) + .ToList(); + + if (members == null || !members.Any()) + return null; + + if (exact) + { + // Try to find exact match with parameters + var parameters = methodInfo.GetParameters(); + var parameterTypes = parameters.Length == 0 + ? string.Empty + : $"({string.Join(",", parameters.Select(GetTypeNameForXmlDoc))})"; + + var exactName = $"M:{typeName}.{methodName}{parameterTypes}"; + return members.FirstOrDefault(m => + string.Equals(m.Attribute("name")?.Value, exactName, StringComparison.Ordinal)); + } + + // Return first match (most specific one) + return members.FirstOrDefault(); + } + + private static string GetTypeNameForXmlDoc(ParameterInfo parameter) + { + return GetTypeNameForXmlDoc(parameter.ParameterType); + } + + private static string GetTypeNameForXmlDoc(Type type) + { + // Handle array types + if (type.IsArray) + { + return $"{GetTypeNameForXmlDoc(type.GetElementType()!)}[]"; + } + + // Handle generic types + if (type.IsGenericType) + { + var name = type.Name.Split('`')[0]; + var args = string.Join(",", type.GetGenericArguments().Select(t => GetTypeNameForXmlDoc(t))); + return $"{name}{{{args}}}"; + } + + // Handle nullable value types + var underlyingType = Nullable.GetUnderlyingType(type); + if (underlyingType != null) + { + return $"{GetTypeNameForXmlDoc(underlyingType)}?"; + } + + // Default case - use full name + return type.FullName ?? type.Name; + } } \ No newline at end of file