-
-
Notifications
You must be signed in to change notification settings - Fork 6
feat(LLMs): add LLMs docs generator project #887
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
Conversation
Reviewer's GuideAdds a new CLI tool project Sequence diagram for CLI-driven docs generation and checksequenceDiagram
actor Developer
participant CLI as llms_docs
participant Program
participant Generator as DocsGenerator
participant Analyzer as ComponentAnalyzer
participant Builder as MarkdownBuilder
participant FS as FileSystem
Developer->>CLI: run with args
CLI->>Program: pass args
Program->>Program: parse options
Program->>Generator: new DocsGenerator(rootFolder, debug)
alt --check option
Program->>Generator: CheckAsync()
Generator->>Analyzer: AnalyzeAllComponentsAsync()
Analyzer->>FS: read component source files
FS-->>Analyzer: source contents and timestamps
Analyzer-->>Generator: List<ComponentInfo>
Generator->>FS: check llms.txt timestamp
Generator->>FS: check components/*.txt timestamps
FS-->>Generator: file timestamps
Generator-->>Program: bool isUpToDate
Program->>Program: set Environment.ExitCode
else --index-only option
Program->>Generator: GenerateIndexAsync()
Generator->>Analyzer: AnalyzeAllComponentsAsync()
Analyzer->>FS: read component source files
FS-->>Analyzer: source contents
Analyzer-->>Generator: List<ComponentInfo>
Generator->>Builder: BuildIndex(components)
Builder-->>Generator: string indexMarkdown
Generator->>FS: write llms.txt
else component specified
Program->>Generator: GenerateComponentAsync(componentName)
Generator->>Analyzer: AnalyzeComponentAsync(componentName)
Analyzer->>FS: read component source
FS-->>Analyzer: source content
Analyzer-->>Generator: ComponentInfo
Generator->>Builder: BuildComponentDoc(component)
Builder-->>Generator: string componentMarkdown
Generator->>FS: write components/ComponentName.txt
else no options
Program->>Generator: GenerateAllAsync()
Generator->>Analyzer: AnalyzeAllComponentsAsync()
Analyzer->>FS: read all component sources
FS-->>Analyzer: source contents
Analyzer-->>Generator: List<ComponentInfo>
Generator->>Builder: BuildIndex(components)
Builder-->>Generator: string indexMarkdown
Generator->>FS: write llms.txt
loop for each ComponentInfo
Generator->>Builder: BuildComponentDoc(component)
Builder-->>Generator: string componentMarkdown
Generator->>FS: write components/ComponentName.txt
end
end
Class diagram for LlmsDocsGenerator core typesclassDiagram
class Program {
+Main(string[] args)
}
class DocsGenerator {
-string _outputPath
-string _componentsOutputPath
-string _sourcePath
-ComponentAnalyzer _analyzer
-MarkdownBuilder _markdownBuilder
-bool _debug
+DocsGenerator(string rootFolder, bool debug)
+Task GenerateAllAsync()
+Task GenerateIndexAsync()
+Task GenerateComponentAsync(string componentName)
+Task<bool> CheckAsync()
}
class ComponentAnalyzer {
-string _sourcePath
-string _componentsPath
-string _samplesPath
+ComponentAnalyzer(string sourcePath)
+Task<List<ComponentInfo>> AnalyzeAllComponentsAsync()
+Task<ComponentInfo?> AnalyzeComponentAsync(string componentName)
}
class MarkdownBuilder {
-string GitHubBaseUrl
-StringBuilder _sb
+string BuildIndex(List<ComponentInfo> components)
+string BuildComponentDoc(ComponentInfo component)
+string BuildParameterTable(List<ParameterInfo> parameters)
}
class ComponentInfo {
+string Name
+string FullName
+string? Summary
+List<string> TypeParameters
+List<ParameterInfo> Parameters
+List<MethodInfo> PublicMethods
+string? BaseClass
+string SourcePath
+DateTime LastModified
+string? SamplePath
}
class ParameterInfo {
+string Name
+string Type
+string? DefaultValue
+string? Description
+bool IsRequired
+bool IsObsolete
+string? ObsoleteMessage
+bool IsEventCallback
}
class MethodInfo {
+string Name
+string ReturnType
+List<(string Type, string Name)> Parameters
+string? Description
+bool IsJSInvokable
}
Program --> DocsGenerator : creates
DocsGenerator --> ComponentAnalyzer : uses
DocsGenerator --> MarkdownBuilder : uses
ComponentAnalyzer --> ComponentInfo : returns
ComponentInfo --> ParameterInfo : contains
ComponentInfo --> MethodInfo : contains
Flow diagram for LlmsDocsGenerator inputs and outputsflowchart LR
A[BootstrapBlazor_source_tree
src/BootstrapBlazor] --> B[ComponentAnalyzer]
B --> C[ComponentInfo_models
Parameters_methods_metadata]
C --> D[MarkdownBuilder]
subgraph DocsGenerator
D --> E[index_content
llms.txt]
D --> F[per_component_docs
components/*.txt]
end
E --> G[wwwroot/llms/llms.txt]
F --> H[wwwroot/llms/components]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey - I've found 5 issues, and left some high level feedback:
- There are two separate implementations of component categorization (
MarkdownBuilder.CategorizeComponentsand the unusedDocsGenerator.GetComponentCategory); consider consolidating this logic into a single shared implementation to avoid divergence over time. - The
ParameterInfo.IsObsolete/ObsoleteMessagefields are populated but then filtered out and never used in the markdown output; either surface obsolete information in the generated docs (e.g., marking deprecated parameters) or remove the fields/logic to keep the model and analyzer minimal.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- There are two separate implementations of component categorization (`MarkdownBuilder.CategorizeComponents` and the unused `DocsGenerator.GetComponentCategory`); consider consolidating this logic into a single shared implementation to avoid divergence over time.
- The `ParameterInfo.IsObsolete`/`ObsoleteMessage` fields are populated but then filtered out and never used in the markdown output; either surface obsolete information in the generated docs (e.g., marking deprecated parameters) or remove the fields/logic to keep the model and analyzer minimal.
## Individual Comments
### Comment 1
<location> `tools/BootstrapBlazor.LLMsDocsGenerator/DocsGenerator.cs:34-43` </location>
<code_context>
+ _markdownBuilder = new MarkdownBuilder();
+ }
+
+ private string FindSourcePath(string? rootFolder)
+ {
+ // Try to find src/BootstrapBlazor from current directory or parent directories
+ var current = rootFolder ?? AppContext.BaseDirectory;
+ Logger($"Root path: {current}");
+
+ while (!string.IsNullOrEmpty(current))
+ {
+ var parent = Directory.GetParent(current);
+ if (parent == null)
+ {
+ break;
+ }
+ if (parent.Name.Equals("BootstrapBlazor", StringComparison.OrdinalIgnoreCase))
+ {
+ return parent.FullName;
</code_context>
<issue_to_address>
**issue (bug_risk):** Passing the repo root via --root is likely to fail the lookup logic in FindSourcePath
Because the loop only checks `parent.Name` for `"BootstrapBlazor"`, passing `--root` as the repo root (whose name is `BootstrapBlazor`) means `parent` becomes the directory above the repo and the check never matches. The search then walks past the repo and eventually throws at the filesystem root.
You can avoid this by also checking the current directory (or treating a non-null `rootFolder` as already resolved), for example:
```csharp
var current = rootFolder ?? AppContext.BaseDirectory;
while (!string.IsNullOrEmpty(current))
{
var dir = new DirectoryInfo(current);
if (dir.Name.Equals("BootstrapBlazor", StringComparison.OrdinalIgnoreCase))
{
return dir.FullName;
}
var parent = dir.Parent;
if (parent == null) break;
current = parent.FullName;
}
```
</issue_to_address>
### Comment 2
<location> `tools/BootstrapBlazor.LLMsDocsGenerator/ComponentAnalyzer.cs:307-316` </location>
<code_context>
+ return value;
+ }
+
+ private string SimplifyTypeName(string typeName)
+ {
+ // Remove common namespace prefixes
+ var result = typeName
+ .Replace("System.", "")
+ .Replace("Collections.Generic.", "")
+ .Replace("Threading.Tasks.", "");
+
+ // Handle Nullable<T> -> T? (must be done carefully to not break other generics)
+ result = NullableRegex().Replace(result, "$1?");
+
+ // Simplify primitive type names
+ result = result
+ .Replace("Int32", "int")
+ .Replace("Int64", "long")
+ .Replace("Boolean", "bool")
+ .Replace("String", "string")
+ .Replace("Object", "object");
+
+ return result;
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Naive string replacements in SimplifyTypeName can corrupt more complex type names
`string.Replace` here can alter substrings inside larger identifiers or namespaces, e.g. `MyInt32Wrapper` → `MyintWrapper`, or a custom namespace containing `System.`. It also won’t robustly handle forms like `global::System.Int32` or nested generics. Prefer deriving the display name from Roslyn symbol info where possible, or at least restrict replacements to whole identifiers (e.g., via regex with word boundaries) to prevent unintended substitutions.
Suggested implementation:
```csharp
private string SimplifyTypeName(string typeName)
{
// First, normalize primitive type names based on identifier tokens (with optional System/global::System prefixes)
var result = PrimitiveTypeRegex().Replace(
typeName,
static m =>
{
// Group 1 contains the primitive identifier without namespace
return m.Groups[1].Value switch
{
"Int32" => "int",
"Int64" => "long",
"Boolean" => "bool",
"String" => "string",
"Object" => "object",
_ => m.Value
};
});
// Handle Nullable<T> -> T? (must be done carefully to not break other generics)
result = NullableRegex().Replace(result, "$1?");
// Remove common namespace prefixes, but only when they appear as whole identifiers
result = SystemNamespaceRegex().Replace(result, string.Empty);
result = CollectionsGenericNamespaceRegex().Replace(result, string.Empty);
result = ThreadingTasksNamespaceRegex().Replace(result, string.Empty);
return result;
}
[GeneratedRegex(@"Nullable<([^>]+)>")]
private static partial Regex NullableRegex();
// Matches primitive CLR types with optional System/global::System prefix:
// Int32
// System.Int32
// global::System.Int32
[GeneratedRegex(@"\b(?:(?:global::)?System\.)?(Int32|Int64|Boolean|String|Object)\b")]
private static partial Regex PrimitiveTypeRegex();
// Match System. / global::System. when used as a namespace (not inside a larger identifier)
[GeneratedRegex(@"\b(?:global::)?System\.")]
private static partial Regex SystemNamespaceRegex();
// Match Collections.Generic. when used as a namespace segment
[GeneratedRegex(@"\bCollections\.Generic\.")]
private static partial Regex CollectionsGenericNamespaceRegex();
// Match Threading.Tasks. when used as a namespace segment
[GeneratedRegex(@"\bThreading\.Tasks\.")]
private static partial Regex ThreadingTasksNamespaceRegex();
```
This change assumes `ComponentAnalyzer` is already declared as a `partial` class to support `partial` regex methods via `GeneratedRegex` (as is already the case for `NullableRegex`). If the class is not partial elsewhere in the file, ensure its declaration includes the `partial` keyword so these generated methods compile correctly.
</issue_to_address>
### Comment 3
<location> `tools/BootstrapBlazor.LLMsDocsGenerator/ComponentAnalyzer.cs:329-330` </location>
<code_context>
+ return result;
+ }
+
+ [GeneratedRegex(@"Nullable<([^>]+)>")]
+ private static partial Regex NullableRegex();
+
+ private bool HasAttribute(MemberDeclarationSyntax member, string attributeName)
</code_context>
<issue_to_address>
**issue (bug_risk):** NullableRegex does not handle nested generic type arguments correctly
The regex stops at the first `>`, so nested generics like `Nullable<List<int>>` won’t be matched correctly and the type string can be partially rewritten, leading to misleading docs. To avoid mangling, either use a more robust approach (e.g., syntax tree/type model) for `Nullable<T>` or explicitly limit the regex to simple cases with no inner `>` and skip more complex types.
</issue_to_address>
### Comment 4
<location> `tools/BootstrapBlazor.LLMsDocsGenerator/DocsGenerator.cs:191-200` </location>
<code_context>
+ private static string GetComponentCategory(string componentName)
</code_context>
<issue_to_address>
**suggestion:** GetComponentCategory in DocsGenerator appears unused and overlaps with MarkdownBuilder logic
This method isn’t used in `DocsGenerator`, and similar categorization logic already exists in `MarkdownBuilder`. Unless there’s an immediate plan to use it, consider removing it or extracting a shared categorization helper for both classes to avoid duplication and dead code.
Suggested implementation:
```csharp
```
1. If `DocsGenerator` later needs component categorization, extract the existing logic from `MarkdownBuilder` into a shared helper (e.g., a static utility class in a shared project/namespace) and have both `DocsGenerator` and `MarkdownBuilder` call that shared helper instead of duplicating the logic.
2. Ensure any future categorization changes are made only in that shared helper to keep behavior consistent across both classes.
</issue_to_address>
### Comment 5
<location> `tools/BootstrapBlazor.LLMsDocsGenerator/README.md:198` </location>
<code_context>
+
+## Output Format
+
+Each component documentation includes:
+
+```markdown
</code_context>
<issue_to_address>
**suggestion (typo):** Minor grammar improvement for "Each component documentation includes".
Consider changing this to either "Each component's documentation includes:" or "Each component documentation file includes:" for more natural phrasing.
```suggestion
Each component's documentation includes:
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| private string FindSourcePath(string? rootFolder) | ||
| { | ||
| // Try to find src/BootstrapBlazor from current directory or parent directories | ||
| var current = rootFolder ?? AppContext.BaseDirectory; | ||
| Logger($"Root path: {current}"); | ||
|
|
||
| while (!string.IsNullOrEmpty(current)) | ||
| { | ||
| var parent = Directory.GetParent(current); | ||
| if (parent == null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): Passing the repo root via --root is likely to fail the lookup logic in FindSourcePath
Because the loop only checks parent.Name for "BootstrapBlazor", passing --root as the repo root (whose name is BootstrapBlazor) means parent becomes the directory above the repo and the check never matches. The search then walks past the repo and eventually throws at the filesystem root.
You can avoid this by also checking the current directory (or treating a non-null rootFolder as already resolved), for example:
var current = rootFolder ?? AppContext.BaseDirectory;
while (!string.IsNullOrEmpty(current))
{
var dir = new DirectoryInfo(current);
if (dir.Name.Equals("BootstrapBlazor", StringComparison.OrdinalIgnoreCase))
{
return dir.FullName;
}
var parent = dir.Parent;
if (parent == null) break;
current = parent.FullName;
}| private string SimplifyTypeName(string typeName) | ||
| { | ||
| // Remove common namespace prefixes | ||
| var result = typeName | ||
| .Replace("System.", "") | ||
| .Replace("Collections.Generic.", "") | ||
| .Replace("Threading.Tasks.", ""); | ||
|
|
||
| // Handle Nullable<T> -> T? (must be done carefully to not break other generics) | ||
| result = NullableRegex().Replace(result, "$1?"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): Naive string replacements in SimplifyTypeName can corrupt more complex type names
string.Replace here can alter substrings inside larger identifiers or namespaces, e.g. MyInt32Wrapper → MyintWrapper, or a custom namespace containing System.. It also won’t robustly handle forms like global::System.Int32 or nested generics. Prefer deriving the display name from Roslyn symbol info where possible, or at least restrict replacements to whole identifiers (e.g., via regex with word boundaries) to prevent unintended substitutions.
Suggested implementation:
private string SimplifyTypeName(string typeName)
{
// First, normalize primitive type names based on identifier tokens (with optional System/global::System prefixes)
var result = PrimitiveTypeRegex().Replace(
typeName,
static m =>
{
// Group 1 contains the primitive identifier without namespace
return m.Groups[1].Value switch
{
"Int32" => "int",
"Int64" => "long",
"Boolean" => "bool",
"String" => "string",
"Object" => "object",
_ => m.Value
};
});
// Handle Nullable<T> -> T? (must be done carefully to not break other generics)
result = NullableRegex().Replace(result, "$1?");
// Remove common namespace prefixes, but only when they appear as whole identifiers
result = SystemNamespaceRegex().Replace(result, string.Empty);
result = CollectionsGenericNamespaceRegex().Replace(result, string.Empty);
result = ThreadingTasksNamespaceRegex().Replace(result, string.Empty);
return result;
}
[GeneratedRegex(@"Nullable<([^>]+)>")]
private static partial Regex NullableRegex();
// Matches primitive CLR types with optional System/global::System prefix:
// Int32
// System.Int32
// global::System.Int32
[GeneratedRegex(@"\b(?:(?:global::)?System\.)?(Int32|Int64|Boolean|String|Object)\b")]
private static partial Regex PrimitiveTypeRegex();
// Match System. / global::System. when used as a namespace (not inside a larger identifier)
[GeneratedRegex(@"\b(?:global::)?System\.")]
private static partial Regex SystemNamespaceRegex();
// Match Collections.Generic. when used as a namespace segment
[GeneratedRegex(@"\bCollections\.Generic\.")]
private static partial Regex CollectionsGenericNamespaceRegex();
// Match Threading.Tasks. when used as a namespace segment
[GeneratedRegex(@"\bThreading\.Tasks\.")]
private static partial Regex ThreadingTasksNamespaceRegex();This change assumes ComponentAnalyzer is already declared as a partial class to support partial regex methods via GeneratedRegex (as is already the case for NullableRegex). If the class is not partial elsewhere in the file, ensure its declaration includes the partial keyword so these generated methods compile correctly.
| [GeneratedRegex(@"Nullable<([^>]+)>")] | ||
| private static partial Regex NullableRegex(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): NullableRegex does not handle nested generic type arguments correctly
The regex stops at the first >, so nested generics like Nullable<List<int>> won’t be matched correctly and the type string can be partially rewritten, leading to misleading docs. To avoid mangling, either use a more robust approach (e.g., syntax tree/type model) for Nullable<T> or explicitly limit the regex to simple cases with no inner > and skip more complex types.
| private static string GetComponentCategory(string componentName) | ||
| { | ||
| return componentName.ToLowerInvariant() switch | ||
| { | ||
| // Table family | ||
| var n when n.Contains("table") => "table", | ||
|
|
||
| // Input family | ||
| var n when n.Contains("input") || n.Contains("textarea") || | ||
| n.Contains("password") || n == "otpinput" => "input", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: GetComponentCategory in DocsGenerator appears unused and overlaps with MarkdownBuilder logic
This method isn’t used in DocsGenerator, and similar categorization logic already exists in MarkdownBuilder. Unless there’s an immediate plan to use it, consider removing it or extracting a shared categorization helper for both classes to avoid duplication and dead code.
Suggested implementation:
- If
DocsGeneratorlater needs component categorization, extract the existing logic fromMarkdownBuilderinto a shared helper (e.g., a static utility class in a shared project/namespace) and have bothDocsGeneratorandMarkdownBuildercall that shared helper instead of duplicating the logic. - Ensure any future categorization changes are made only in that shared helper to keep behavior consistent across both classes.
|
|
||
| ## Output Format | ||
|
|
||
| Each component documentation includes: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (typo): Minor grammar improvement for "Each component documentation includes".
Consider changing this to either "Each component's documentation includes:" or "Each component documentation file includes:" for more natural phrasing.
| Each component documentation includes: | |
| Each component's documentation includes: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a new tool called LLMsDocsGenerator that automatically generates LLM-friendly documentation for BootstrapBlazor components by analyzing source code using Roslyn and creating structured Markdown files.
Key Changes:
- Adds a .NET global tool that uses Roslyn to extract component parameters, methods, and documentation from source code
- Generates individual documentation files per component (components/*.txt) plus a master index (llms.txt) for LLM consumption
- Provides CI/CD integration capabilities to ensure documentation stays synchronized with code changes
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/BootstrapBlazor.LLMsDocsGenerator/README.md | English documentation explaining the tool's purpose, architecture, and usage |
| tools/BootstrapBlazor.LLMsDocsGenerator/README.zh-CN.md | Chinese translation of the documentation |
| tools/BootstrapBlazor.LLMsDocsGenerator/Program.cs | CLI entry point with command-line argument parsing |
| tools/BootstrapBlazor.LLMsDocsGenerator/MarkdownBuilder.cs | Generates Markdown documentation from component metadata |
| tools/BootstrapBlazor.LLMsDocsGenerator/DocsGenerator.cs | Orchestrates the documentation generation workflow |
| tools/BootstrapBlazor.LLMsDocsGenerator/ComponentInfo.cs | Data models for component, parameter, and method information |
| tools/BootstrapBlazor.LLMsDocsGenerator/ComponentAnalyzer.cs | Roslyn-based analyzer that extracts component information from C# source files |
| tools/BootstrapBlazor.LLMsDocsGenerator/BootstrapBlazor.LLMsDocsGenerator.csproj | Project configuration defining the tool as a .NET global tool |
| BootstrapBlazor.Extensions.slnx | Adds the new project to the solution |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private static string GetComponentCategory(string componentName) | ||
| { | ||
| return componentName.ToLowerInvariant() switch | ||
| { | ||
| // Table family | ||
| var n when n.Contains("table") => "table", | ||
|
|
||
| // Input family | ||
| var n when n.Contains("input") || n.Contains("textarea") || | ||
| n.Contains("password") || n == "otpinput" => "input", | ||
|
|
||
| // Select family | ||
| var n when n.Contains("select") || n.Contains("dropdown") || | ||
| n.Contains("autocomplete") || n.Contains("cascader") || | ||
| n.Contains("transfer") || n.Contains("multiselect") => "select", | ||
|
|
||
| // Button family | ||
| var n when n.Contains("button") || n == "gotop" || | ||
| n.Contains("popconfirm") => "button", | ||
|
|
||
| // Dialog family | ||
| var n when n.Contains("dialog") || n.Contains("modal") || | ||
| n.Contains("drawer") || n.Contains("swal") || | ||
| n.Contains("toast") || n.Contains("message") => "dialog", | ||
|
|
||
| // Navigation family | ||
| var n when n.Contains("menu") || n.Contains("tab") || | ||
| n.Contains("breadcrumb") || n.Contains("step") || | ||
| n.Contains("anchor") || n.Contains("nav") => "nav", | ||
|
|
||
| // Card/Container family | ||
| var n when n.Contains("card") || n.Contains("collapse") || | ||
| n.Contains("groupbox") || n.Contains("panel") => "card", | ||
|
|
||
| // TreeView | ||
| var n when n.Contains("tree") => "treeview", | ||
|
|
||
| // Form | ||
| var n when n.Contains("validateform") || n.Contains("editorform") || | ||
| n.Contains("validator") => "form", | ||
|
|
||
| _ => "other" | ||
| }; | ||
| } | ||
|
|
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method GetComponentCategory is defined but never called within DocsGenerator.cs. The categorization logic is actually performed in MarkdownBuilder.CategorizeComponents. This unused method adds confusion and should be removed to improve code clarity.
| private static string GetComponentCategory(string componentName) | |
| { | |
| return componentName.ToLowerInvariant() switch | |
| { | |
| // Table family | |
| var n when n.Contains("table") => "table", | |
| // Input family | |
| var n when n.Contains("input") || n.Contains("textarea") || | |
| n.Contains("password") || n == "otpinput" => "input", | |
| // Select family | |
| var n when n.Contains("select") || n.Contains("dropdown") || | |
| n.Contains("autocomplete") || n.Contains("cascader") || | |
| n.Contains("transfer") || n.Contains("multiselect") => "select", | |
| // Button family | |
| var n when n.Contains("button") || n == "gotop" || | |
| n.Contains("popconfirm") => "button", | |
| // Dialog family | |
| var n when n.Contains("dialog") || n.Contains("modal") || | |
| n.Contains("drawer") || n.Contains("swal") || | |
| n.Contains("toast") || n.Contains("message") => "dialog", | |
| // Navigation family | |
| var n when n.Contains("menu") || n.Contains("tab") || | |
| n.Contains("breadcrumb") || n.Contains("step") || | |
| n.Contains("anchor") || n.Contains("nav") => "nav", | |
| // Card/Container family | |
| var n when n.Contains("card") || n.Contains("collapse") || | |
| n.Contains("groupbox") || n.Contains("panel") => "card", | |
| // TreeView | |
| var n when n.Contains("tree") => "treeview", | |
| // Form | |
| var n when n.Contains("validateform") || n.Contains("editorform") || | |
| n.Contains("validator") => "form", | |
| _ => "other" | |
| }; | |
| } |
| { | ||
| Console.WriteLine($"Components directory not found: {_componentsPath}"); |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Console.WriteLine call on line 38 should respect the debug flag. Currently, this message is always printed even when not in debug mode. Consider using the Logger method instead to maintain consistency with the rest of the application's logging behavior.
| { | |
| Console.WriteLine($"Components directory not found: {_componentsPath}"); | |
| { | |
| #if DEBUG | |
| Console.WriteLine($"Components directory not found: {_componentsPath}"); | |
| #endif |
| } | ||
| catch (Exception ex) | ||
| { | ||
| Console.WriteLine($"Error analyzing {filePath}: {ex.Message}"); |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Console.WriteLine call on line 131 should use a structured logging approach or at minimum include more context about the error. Consider including the file path and more detailed error information to help with debugging when component analysis fails.
| Console.WriteLine($"Error analyzing {filePath}: {ex.Message}"); | |
| Console.Error.WriteLine( | |
| $"Error analyzing component file. filePath=\"{filePath}\", exceptionType=\"{ex.GetType().FullName}\", exception={ex}"); |
| var componentOption = new Option<string?>("--component") { Description = "Generate documentation for a specific component only" }; | ||
| var indexOnlyOption = new Option<bool>("--index-only") { Description = "Generate only the index file (llms.txt)" }; | ||
| var checkOption = new Option<bool>("--check") { Description = "Check if documentation is up-to-date (for CI/CD)" }; | ||
| var rootFolderOption = new Option<string?>("--root") { Description = "Set the root folder of project" }; |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Description property of Option instances should end with a period for consistency and proper grammar. All option descriptions should follow the same punctuation style.
| var componentOption = new Option<string?>("--component") { Description = "Generate documentation for a specific component only" }; | |
| var indexOnlyOption = new Option<bool>("--index-only") { Description = "Generate only the index file (llms.txt)" }; | |
| var checkOption = new Option<bool>("--check") { Description = "Check if documentation is up-to-date (for CI/CD)" }; | |
| var rootFolderOption = new Option<string?>("--root") { Description = "Set the root folder of project" }; | |
| var componentOption = new Option<string?>("--component") { Description = "Generate documentation for a specific component only." }; | |
| var indexOnlyOption = new Option<bool>("--index-only") { Description = "Generate only the index file (llms.txt)." }; | |
| var checkOption = new Option<bool>("--check") { Description = "Check if documentation is up-to-date (for CI/CD)." }; | |
| var rootFolderOption = new Option<string?>("--root") { Description = "Set the root folder of project." }; |
| rootCommand.SetAction(async result => | ||
| { | ||
| var debug = result.GetValue(debugOption); | ||
| var rootFolder = result.GetValue(rootFolderOption); | ||
| var generator = new DocsGenerator(rootFolder, debug); | ||
|
|
||
| var check = result.GetValue(checkOption); | ||
| if (check) | ||
| { | ||
| var isUpToDate = await generator.CheckAsync(); | ||
| Environment.ExitCode = isUpToDate ? 0 : 1; | ||
| return; | ||
| } | ||
|
|
||
| var indexOnly = result.GetValue(indexOnlyOption); | ||
| if (indexOnly) | ||
| { | ||
| await generator.GenerateIndexAsync(); | ||
| return; | ||
| } | ||
|
|
||
| var component = result.GetValue(componentOption); | ||
| if (!string.IsNullOrEmpty(component)) | ||
| { | ||
| await generator.GenerateComponentAsync(component); | ||
| return; | ||
| } | ||
|
|
||
| await generator.GenerateAllAsync(); | ||
| }); |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method uses SetAction which is not a standard method on RootCommand in System.CommandLine. The standard approach is to set the Handler property or use SetHandler. This code will likely fail at runtime.
| private void Logger(string message) | ||
| { | ||
| if (_debug) | ||
| { | ||
| Console.WriteLine(message); | ||
| } |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Logger method only outputs when debug is true, but in non-debug mode, users won't see any output including important messages like "Component not found" or error messages. Consider always outputting critical messages (errors, warnings) regardless of the debug flag, and only suppress verbose/informational messages.
| private void Logger(string message) | |
| { | |
| if (_debug) | |
| { | |
| Console.WriteLine(message); | |
| } | |
| private enum LogLevel | |
| { | |
| Info, | |
| Warning, | |
| Error | |
| } | |
| private void Logger(string message, LogLevel level = LogLevel.Info) | |
| { | |
| // In debug mode, log everything. | |
| if (_debug) | |
| { | |
| Console.WriteLine(message); | |
| return; | |
| } | |
| // In non-debug mode, only log warnings and errors. | |
| if (level == LogLevel.Warning || level == LogLevel.Error) | |
| { | |
| Console.WriteLine(message); | |
| } |
| foreach (var method in methodDeclarations) | ||
| { | ||
| // Skip property accessors, overrides of base class methods | ||
| if (method.Modifiers.Any(m => m.IsKind(SyntaxKind.OverrideKeyword))) | ||
| continue; | ||
|
|
||
| var methodInfo = new MethodInfo | ||
| { | ||
| Name = method.Identifier.Text, | ||
| ReturnType = SimplifyTypeName(method.ReturnType.ToString()), | ||
| Description = ExtractXmlSummary(method), | ||
| IsJSInvokable = HasAttribute(method, "JSInvokable"), | ||
| Parameters = method.ParameterList.Parameters | ||
| .Select(p => (SimplifyTypeName(p.Type?.ToString() ?? ""), p.Identifier.Text)) | ||
| .ToList() | ||
| }; | ||
|
|
||
| methods.Add(methodInfo); | ||
| } |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| foreach (var file in files) | ||
| { | ||
| var component = await AnalyzeFileAsync(file); | ||
| if (component != null && component.Parameters.Count > 0) | ||
| { | ||
| components.Add(component); | ||
| } | ||
| } |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.
| foreach (var file in files) | ||
| { | ||
| var component = await AnalyzeFileAsync(file); | ||
| if (component != null && component.Parameters.Count > 0) | ||
| { | ||
| components.Add(component); | ||
| } | ||
| } | ||
|
|
||
| // Also analyze .cs files that might be component base classes | ||
| var csFiles = Directory.GetFiles(_componentsPath, "*Base.cs", SearchOption.AllDirectories); | ||
| foreach (var file in csFiles) | ||
| { | ||
| var component = await AnalyzeFileAsync(file); | ||
| if (component != null && component.Parameters.Count > 0) | ||
| { | ||
| components.Add(component); | ||
| } | ||
| } |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.
| foreach (var file in files) | |
| { | |
| var component = await AnalyzeFileAsync(file); | |
| if (component != null && component.Parameters.Count > 0) | |
| { | |
| components.Add(component); | |
| } | |
| } | |
| // Also analyze .cs files that might be component base classes | |
| var csFiles = Directory.GetFiles(_componentsPath, "*Base.cs", SearchOption.AllDirectories); | |
| foreach (var file in csFiles) | |
| { | |
| var component = await AnalyzeFileAsync(file); | |
| if (component != null && component.Parameters.Count > 0) | |
| { | |
| components.Add(component); | |
| } | |
| } | |
| var fileAnalysisTasks = files.Select(file => AnalyzeFileAsync(file)); | |
| var fileAnalysisResults = await Task.WhenAll(fileAnalysisTasks); | |
| components.AddRange(fileAnalysisResults | |
| .Where(component => component != null && component.Parameters.Count > 0)!); | |
| // Also analyze .cs files that might be component base classes | |
| var csFiles = Directory.GetFiles(_componentsPath, "*Base.cs", SearchOption.AllDirectories); | |
| var csFileAnalysisTasks = csFiles.Select(file => AnalyzeFileAsync(file)); | |
| var csFileAnalysisResults = await Task.WhenAll(csFileAnalysisTasks); | |
| components.AddRange(csFileAnalysisResults | |
| .Where(component => component != null && component.Parameters.Count > 0)!); |
|
|
||
| if (classDeclaration.TypeParameterList != null) | ||
| { | ||
| className += classDeclaration.TypeParameterList.ToString(); |
Copilot
AI
Jan 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant call to 'ToString' on a String object.
Link issues
fixes #886
Summary By Copilot
Regression?
Risk
Verification
Packaging changes reviewed?
☑️ Self Check before Merge
Summary by Sourcery
Introduce a new LLM-oriented documentation generator tool for BootstrapBlazor components, including Roslyn-based analysis, markdown generation, and a CLI entrypoint.
New Features: