WIP: Generate XML documentation from DocumentationAttribute for IDE IntelliSense #12
@@ -0,0 +1,260 @@
|
||||
# Do We Need IDE Plugins for DocumentationAttribute?
|
||||
|
||||
## The Question
|
||||
|
||||
Should we create plugins for Visual Studio and Rider to read `DocumentationAttribute` directly and display it in IDE tooltips?
|
||||
|
||||
## Quick Answer
|
||||
|
||||
**NO - Plugins are NOT necessary or recommended.** The standard .NET approach with triple-slash comments is better.
|
||||
|
||||
## Why Plugins Are Not The Right Solution
|
||||
|
||||
### 1. Complexity vs. Benefit
|
||||
|
||||
**Plugin Development Effort:**
|
||||
- ✅ Visual Studio plugin: Complex, requires VSIX development
|
||||
- ✅ Rider plugin: Requires Kotlin/Java, ReSharper SDK knowledge
|
||||
- ✅ VS Code plugin: TypeScript/JavaScript, OmniSharp integration
|
||||
- ❌ Maintenance burden: Updates for each IDE version
|
||||
- ❌ Testing: Must test across multiple IDE versions
|
||||
- ❌ Distribution: Users must install plugins manually
|
||||
|
||||
**Standard Approach:**
|
||||
- ✅ Triple-slash comments: Built into language
|
||||
- ✅ XML documentation: MSBuild native support
|
||||
- ✅ Works everywhere: All IDEs, all tools
|
||||
- ✅ Zero installation: Just works
|
||||
- ✅ Zero maintenance: Standard .NET
|
||||
|
||||
### 2. Limited Adoption
|
||||
|
||||
**Plugin Reality:**
|
||||
- Users must discover the plugin exists
|
||||
- Users must manually install it
|
||||
- Users must trust third-party plugin
|
||||
- Users must update it regularly
|
||||
- Many users won't bother
|
||||
|
||||
**Standard Approach:**
|
||||
- Works for everyone immediately
|
||||
- No installation needed
|
||||
- No trust barrier
|
||||
- Standard .NET practice
|
||||
|
||||
### 3. IDE Extension Points Limitations
|
||||
|
||||
#### Visual Studio
|
||||
|
||||
**Documentation Provider API:**
|
||||
```csharp
|
||||
// VS can extend Quick Info via IAsyncQuickInfoSourceProvider
|
||||
// But this is complex and has limitations
|
||||
```
|
||||
|
||||
**Limitations:**
|
||||
- Only works in VS (not VS Code, not Rider)
|
||||
- Requires COM interop or MEF
|
||||
- Performance concerns
|
||||
- Must handle all edge cases
|
||||
- Complex debugging
|
||||
|
||||
#### JetBrains Rider
|
||||
|
||||
**PSI (Program Structure Interface):**
|
||||
```kotlin
|
||||
// Rider can extend documentation via QuickDoc providers
|
||||
// Requires ReSharper SDK
|
||||
```
|
||||
|
||||
**Limitations:**
|
||||
- Kotlin/Java development
|
||||
- Different from VS approach
|
||||
- Must understand Rider's PSI
|
||||
- Complex plugin architecture
|
||||
|
||||
#### VS Code
|
||||
|
||||
**Language Server Protocol:**
|
||||
```typescript
|
||||
// Must extend OmniSharp or create custom language server
|
||||
```
|
||||
|
||||
**Limitations:**
|
||||
- Requires LSP knowledge
|
||||
- Different architecture again
|
||||
- Must integrate with C# extension
|
||||
|
||||
### 4. The Standard Solution Works Better
|
||||
|
||||
**What We Have:**
|
||||
```csharp
|
||||
[Documentation("TryGetValue", "Tries to get the value...", null, "code example")]
|
||||
/// <summary>
|
||||
/// Tries to get the value from the NeoDictionary for the given key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to search for</param>
|
||||
/// <param name="value">The value associated with the key, if found</param>
|
||||
/// <returns>True if the key was found, false otherwise</returns>
|
||||
public bool TryGetValue(TKey key, out TValue value)
|
||||
```
|
||||
|
||||
**How It Works:**
|
||||
|
||||
1. **In IDE (Developer View):**
|
||||
- Rider/VS reads `/// <summary>` from source
|
||||
- Shows immediately in tooltips
|
||||
- No plugin needed
|
||||
|
||||
2. **In XML (Consumer View):**
|
||||
- XmlDocGenerator reads DocumentationAttribute
|
||||
- Augments XML with examples, extended info
|
||||
- Consumers get rich documentation
|
||||
|
||||
3. **Result:**
|
||||
- ✅ Best of both worlds
|
||||
- ✅ Zero plugins needed
|
||||
- ✅ Standard .NET practice
|
||||
|
||||
## What DocumentationAttribute Provides
|
||||
|
||||
The attribute is still valuable for:
|
||||
|
||||
### 1. Code Examples
|
||||
```csharp
|
||||
[Documentation(..., codeExample: @"```csharp
|
||||
var dict = new NeoDictionary<string, int>();
|
||||
dict.Add(""key"", 42);
|
||||
```")]
|
||||
```
|
||||
|
||||
This goes into `<example>` in XML, which IDEs show in extended documentation.
|
||||
|
||||
### 2. Structured Metadata
|
||||
```csharp
|
||||
[Documentation(..., args: new[] {
|
||||
"key: The unique identifier",
|
||||
"value: The output value"
|
||||
})]
|
||||
```
|
||||
|
||||
XmlDocGenerator converts this to proper `<param>` tags.
|
||||
|
||||
### 3. Build-Time Generation
|
||||
- Processes entire assembly
|
||||
- Generates consistent documentation
|
||||
- Can enforce patterns
|
||||
- Single source of truth for metadata
|
||||
|
||||
## Alternative: Roslyn Analyzer
|
||||
|
||||
If we want IDE integration, a **Roslyn Analyzer** is better than a plugin:
|
||||
|
||||
### Benefits
|
||||
|
||||
✅ **Works in all IDEs** that support Roslyn (VS, Rider, VS Code)
|
||||
✅ **No installation** - ships with NuGet package
|
||||
✅ **Compile-time warnings** - enforces documentation standards
|
||||
✅ **Code fixes** - can generate `///` from attributes
|
||||
✅ **Standard approach** - many libraries do this
|
||||
|
||||
### Example
|
||||
|
||||
```csharp
|
||||
// Analyzer detects missing /// when DocumentationAttribute exists
|
||||
[Documentation("MyMethod", "Does something")]
|
||||
public void MyMethod() { } // Warning: Add /// summary from Documentation attribute
|
||||
|
||||
// Code fix can generate:
|
||||
[Documentation("MyMethod", "Does something")]
|
||||
/// <summary>
|
||||
/// Does something
|
||||
/// </summary>
|
||||
public void MyMethod() { }
|
||||
```
|
||||
|
||||
### Implementation
|
||||
|
||||
```csharp
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public class DocumentationAttributeAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
// Detect: Has DocumentationAttribute but no /// summary
|
||||
// Suggest: Add /// summary matching the attribute
|
||||
}
|
||||
|
||||
[ExportCodeFixProvider]
|
||||
public class DocumentationAttributeCodeFix : CodeFixProvider
|
||||
{
|
||||
// Generate /// <summary> from DocumentationAttribute
|
||||
}
|
||||
```
|
||||
|
||||
**Distribution:**
|
||||
- Package with OmegaLeo.HelperLib.Documentation
|
||||
- Automatically available to developers
|
||||
- No separate installation
|
||||
|
||||
## Recommendation
|
||||
|
||||
### For Immediate Solution: Add Triple-Slash Comments ✅
|
||||
|
||||
```csharp
|
||||
[Documentation("TryGetValue", "Tries to get the value...")]
|
||||
/// <summary>
|
||||
/// Tries to get the value from the NeoDictionary for the given key.
|
||||
/// </summary>
|
||||
public bool TryGetValue(TKey key, out TValue value)
|
||||
```
|
||||
|
||||
**Why:**
|
||||
- Works immediately
|
||||
- Standard .NET practice
|
||||
- Zero maintenance
|
||||
- Universal compatibility
|
||||
|
||||
### For Future Enhancement: Roslyn Analyzer
|
||||
|
||||
Create an analyzer that:
|
||||
1. Warns when `[Documentation]` exists without `///`
|
||||
2. Provides code fix to generate `///` from attribute
|
||||
3. Ships with NuGet package
|
||||
4. Works in all IDEs
|
||||
|
||||
**Why:**
|
||||
- Better than IDE plugins
|
||||
- Works everywhere Roslyn works
|
||||
- Standard extensibility model
|
||||
- Low maintenance
|
||||
|
||||
### Do NOT Create IDE Plugins ❌
|
||||
|
||||
**Reasons:**
|
||||
- Too complex for the benefit
|
||||
- Limited adoption
|
||||
- Maintenance burden
|
||||
- Non-standard approach
|
||||
- Triple-slash comments solve the problem
|
||||
|
||||
## Comparison Matrix
|
||||
|
||||
| Approach | IDE Support | Installation | Maintenance | Adoption | Recommendation |
|
||||
|----------|-------------|--------------|-------------|----------|----------------|
|
||||
| **Triple-slash `///`** | All IDEs | None | None | ✅ Universal | ✅ **USE THIS** |
|
||||
| **XML Files** | All IDEs | None | Low | ✅ Standard | ✅ Already done |
|
||||
| **Roslyn Analyzer** | VS, Rider, VS Code | Auto (NuGet) | Low | ✅ High | ⚠️ Future enhancement |
|
||||
| **VS Plugin** | VS only | Manual | High | ❌ Low | ❌ **DON'T DO** |
|
||||
| **Rider Plugin** | Rider only | Manual | High | ❌ Low | ❌ **DON'T DO** |
|
||||
| **VS Code Plugin** | VS Code only | Manual | Medium | ❌ Low | ❌ **DON'T DO** |
|
||||
|
||||
## Conclusion
|
||||
|
||||
**No plugins needed.** The standard .NET approach works better:
|
||||
|
||||
1. ✅ Add `/// <summary>` comments to source code
|
||||
2. ✅ Keep `[Documentation]` attributes for extended metadata
|
||||
3. ✅ XmlDocGenerator augments XML with attribute content
|
||||
4. ✅ Everyone sees documentation in their IDE
|
||||
5. ✅ No installation or plugins required
|
||||
|
||||
If we want to enhance the developer experience, create a **Roslyn Analyzer** (not IDE plugins) that helps keep `///` and `[Documentation]` in sync.
|
||||
@@ -12,6 +12,9 @@ BenchmarkUtility.Stop(""MyBenchmark"");
|
||||
var results = BenchmarkUtility.GetResults(""MyBenchmark"");
|
||||
```")]
|
||||
[Changelog("1.2.0", "Fixed root namespace to OmegaLeo.HelperLib.Helpers.", "January 28, 2026")]
|
||||
/// <summary>
|
||||
/// Utility class for benchmarking code execution time.
|
||||
/// </summary>
|
||||
public class BenchmarkUtility
|
||||
{
|
||||
private static Dictionary<string, List<long>> _benchmarks = new Dictionary<string, List<long>>();
|
||||
@@ -83,12 +86,20 @@ Console.WriteLine($""Average time: {averageTime} ms"");
|
||||
}
|
||||
|
||||
[Documentation("Start", "Starts or restarts the stopwatch for the given key.", null, null)]
|
||||
/// <summary>
|
||||
/// Starts or restarts the stopwatch for the given key.
|
||||
/// </summary>
|
||||
/// <param name="key">The benchmark identifier</param>
|
||||
public static void Start(string key)
|
||||
{
|
||||
GetStopwatch(key).Restart();
|
||||
}
|
||||
|
||||
[Documentation("Stop", "Stops the stopwatch for the given key and records the elapsed time.", null, null)]
|
||||
/// <summary>
|
||||
/// Stops the stopwatch for the given key and records the elapsed time.
|
||||
/// </summary>
|
||||
/// <param name="key">The benchmark identifier</param>
|
||||
public static void Stop(string key)
|
||||
{
|
||||
if (!_benchmarks.ContainsKey(key))
|
||||
@@ -101,6 +112,11 @@ Console.WriteLine($""Average time: {averageTime} ms"");
|
||||
}
|
||||
|
||||
[Documentation("GetResults", "Retrieves the list of recorded times for the given key.", null, null)]
|
||||
/// <summary>
|
||||
/// Retrieves the list of recorded times for the given key.
|
||||
/// </summary>
|
||||
/// <param name="key">The benchmark identifier</param>
|
||||
/// <returns>List of recorded times in milliseconds</returns>
|
||||
public static List<long> GetResults(string key)
|
||||
{
|
||||
return _benchmarks.ContainsKey(key) ? _benchmarks[key] : new List<long>();
|
||||
|
||||
@@ -10,6 +10,11 @@ namespace OmegaLeo.HelperLib.Models
|
||||
[Serializable]
|
||||
[Documentation("NeoDictionary", "Dictionary like class created to make it easier to display dictionaries in game engines like Unity")]
|
||||
[Changelog("1.2.0", "Fixed root namespace to OmegaLeo.HelperLib.Models.", "January 28, 2026")]
|
||||
/// <summary>
|
||||
/// Dictionary like class created to make it easier to display dictionaries in game engines like Unity.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of keys in the dictionary</typeparam>
|
||||
/// <typeparam name="TValue">The type of values in the dictionary</typeparam>
|
||||
public class NeoDictionary<TKey,TValue>
|
||||
{
|
||||
public List<NeoDictionaryItem<TKey, TValue>> Items = new List<NeoDictionaryItem<TKey, TValue>>();
|
||||
@@ -23,6 +28,12 @@ namespace OmegaLeo.HelperLib.Models
|
||||
: new Dictionary<TKey, TValue>();
|
||||
|
||||
[Documentation("TryGetValue", "Tries to get the value from the NeoDictionary for the given key.")]
|
||||
/// <summary>
|
||||
/// Tries to get the value from the NeoDictionary for the given key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to search for</param>
|
||||
/// <param name="value">The value associated with the key, if found</param>
|
||||
/// <returns>True if the key was found, false otherwise</returns>
|
||||
public bool TryGetValue(TKey key, out TValue value)
|
||||
{
|
||||
var item = Items.FirstOrDefault(x => x.Key.Equals(key));
|
||||
@@ -39,6 +50,12 @@ namespace OmegaLeo.HelperLib.Models
|
||||
}
|
||||
|
||||
[Documentation("TryGetValueFromIndex", "Tries to get the value from the NeoDictionary at the given index.")]
|
||||
/// <summary>
|
||||
/// Tries to get the value from the NeoDictionary at the given index.
|
||||
/// </summary>
|
||||
/// <param name="index">The zero-based index of the element</param>
|
||||
/// <param name="value">The value at the specified index, if found</param>
|
||||
/// <returns>True if the index is valid, false otherwise</returns>
|
||||
public bool TryGetValueFromIndex(int index, out TValue value)
|
||||
{
|
||||
if (index < Items.Count)
|
||||
@@ -58,6 +75,11 @@ namespace OmegaLeo.HelperLib.Models
|
||||
}
|
||||
|
||||
[Documentation("Add", "Adds a new NeoDictionaryItem to the NeoDictionary.")]
|
||||
/// <summary>
|
||||
/// Adds a new NeoDictionaryItem to the NeoDictionary.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element to add</param>
|
||||
/// <param name="value">The value of the element to add</param>
|
||||
public void Add(TKey key, TValue value)
|
||||
{
|
||||
Items.Add(new NeoDictionaryItem<TKey, TValue>(key, value));
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# Why Rider Shows Documentation Attribute Instead of XML Documentation
|
||||
|
||||
## The Problem
|
||||
|
||||
When working in the HelperLib repository, hovering over methods in Rider shows:
|
||||
|
||||
```csharp
|
||||
[Documentation("TryGetValue", "Tries to get the value...", null, "")]
|
||||
public bool TryGetValue(TKey key, out TValue value)
|
||||
```
|
||||
|
||||
Instead of rich IntelliSense documentation.
|
||||
|
||||
## Root Cause: Source Code Takes Priority
|
||||
|
||||
### How IDEs Resolve Documentation
|
||||
|
||||
IDEs like Rider, Visual Studio, and VS Code follow this priority order:
|
||||
|
||||
1. **Triple-slash comments (`///`) in source code** ← HIGHEST PRIORITY
|
||||
2. XML documentation files (`.xml`)
|
||||
3. Decompiled/reflected information
|
||||
4. Raw signature
|
||||
|
||||
### What's Happening in HelperLib
|
||||
|
||||
Current state:
|
||||
```csharp
|
||||
[Documentation("TryGetValue", "Tries to get the value...")]
|
||||
public bool TryGetValue(TKey key, out TValue value)
|
||||
{
|
||||
// No /// comments here!
|
||||
}
|
||||
```
|
||||
|
||||
**The test project uses `<ProjectReference>`:**
|
||||
- Rider can access the source code directly
|
||||
- No `///` comments exist in source
|
||||
- Rider shows the raw source code (including the attribute)
|
||||
- XML file is ignored because source is available
|
||||
|
||||
## The Solution: Add Triple-Slash Comments
|
||||
|
||||
### Option 1: Minimal Comments (Recommended)
|
||||
|
||||
Add simple `/// <summary>` that references the documentation:
|
||||
|
||||
```csharp
|
||||
[Documentation("TryGetValue", "Tries to get the value from the NeoDictionary for the given key.")]
|
||||
/// <summary>
|
||||
/// Tries to get the value from the NeoDictionary for the given key.
|
||||
/// </summary>
|
||||
public bool TryGetValue(TKey key, out TValue value)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ Rider shows documentation immediately
|
||||
- ✅ Works for developers with source access
|
||||
- ✅ XML generator can still augment with additional info
|
||||
- ✅ Standard .NET practice
|
||||
|
||||
### Option 2: Generated Comments (Advanced)
|
||||
|
||||
Create a Source Generator that reads `DocumentationAttribute` and generates `/// <summary>` comments automatically.
|
||||
|
||||
**Benefits:**
|
||||
- ✅ Single source of truth (the attribute)
|
||||
- ✅ No duplication
|
||||
|
||||
**Drawbacks:**
|
||||
- ❌ More complex
|
||||
- ❌ Requires Source Generator infrastructure
|
||||
- ❌ Comments generated at compile-time (not visible in source)
|
||||
|
||||
### Option 3: Accept Current Behavior
|
||||
|
||||
Keep as-is and document that developers see attributes, consumers see XML.
|
||||
|
||||
**Benefits:**
|
||||
- ✅ No code changes needed
|
||||
- ✅ XML documentation works perfectly for NuGet consumers
|
||||
|
||||
**Drawbacks:**
|
||||
- ❌ Poor developer experience in IDE
|
||||
- ❌ Makes development harder
|
||||
|
||||
## Why This Matters
|
||||
|
||||
### For Library Developers (Us)
|
||||
|
||||
Working on HelperLib with ProjectReference:
|
||||
- ❌ **Without `///`:** See raw attributes, no IntelliSense
|
||||
- ✅ **With `///`:** See rich documentation in IDE
|
||||
|
||||
### For Library Consumers
|
||||
|
||||
Installing HelperLib NuGet package:
|
||||
- ✅ Only have DLL + XML
|
||||
- ✅ Always see rich documentation
|
||||
- ✅ Not affected by source code
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Add `/// <summary>` comments to all public APIs.**
|
||||
|
||||
This is standard practice in .NET libraries:
|
||||
- Microsoft does this (see .NET source code)
|
||||
- Popular libraries do this (Newtonsoft.Json, etc.)
|
||||
- Provides best developer experience
|
||||
|
||||
The DocumentationAttribute can still provide additional metadata:
|
||||
- Code examples
|
||||
- Extended remarks
|
||||
- Structured argument descriptions
|
||||
|
||||
The XmlDocGenerator augments the XML with this additional content.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
1. Add `/// <summary>` to all public types and members
|
||||
2. Keep DocumentationAttribute for extended metadata
|
||||
3. XmlDocGenerator augments XML with attribute data
|
||||
4. Best of both worlds:
|
||||
- Developers see documentation in IDE
|
||||
- XML files have rich content from attributes
|
||||
- Consumers get full documentation
|
||||
|
||||
## Example
|
||||
|
||||
**Before:**
|
||||
```csharp
|
||||
[Documentation("BenchmarkUtility", "Utility class for benchmarking code execution time.")]
|
||||
public class BenchmarkUtility
|
||||
{
|
||||
[Documentation("Start", "Starts or restarts the stopwatch for the given key.")]
|
||||
public static void Start(string key)
|
||||
```
|
||||
|
||||
**After:**
|
||||
```csharp
|
||||
[Documentation("BenchmarkUtility", "Utility class for benchmarking code execution time.", null, @"```csharp
|
||||
BenchmarkUtility.Start(""MyBenchmark"");
|
||||
// Code to benchmark
|
||||
BenchmarkUtility.Stop(""MyBenchmark"");
|
||||
```")]
|
||||
/// <summary>
|
||||
/// Utility class for benchmarking code execution time.
|
||||
/// </summary>
|
||||
public class BenchmarkUtility
|
||||
{
|
||||
[Documentation("Start", "Starts or restarts the stopwatch for the given key.")]
|
||||
/// <summary>
|
||||
/// Starts or restarts the stopwatch for the given key.
|
||||
/// </summary>
|
||||
/// <param name="key">The benchmark identifier</param>
|
||||
public static void Start(string key)
|
||||
```
|
||||
|
||||
Result:
|
||||
- ✅ Rider shows documentation immediately
|
||||
- ✅ XML file has base documentation
|
||||
- ✅ XmlDocGenerator adds code examples from attribute
|
||||
- ✅ Perfect IDE experience for everyone
|
||||
|
||||
## Automated Solution
|
||||
|
||||
Could create a tool or analyzer that:
|
||||
1. Reads DocumentationAttribute
|
||||
2. Warns if no `/// <summary>` exists
|
||||
3. Suggests adding the summary from the attribute
|
||||
|
||||
This enforces the pattern and maintains single source of truth.
|
||||
Reference in New Issue
Block a user