11// -----------------------------------------------------------------------
2- // 作者:Mud Studio 版权所有 (c) Mud Studio 2026
2+ // 作者:Mud Studio 版权所有 (c) Mud Studio 2026
33// Mud.HttpUtils 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
44// 本项目主要遵循 MIT 许可证进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 文件。
55// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目开发而产生的一切法律纠纷和责任,我们不承担任何责任!
66// -----------------------------------------------------------------------
77
88using System . Collections . Immutable ;
99using System . Composition ;
10+ using System . Text ;
1011using Microsoft . CodeAnalysis ;
1112using Microsoft . CodeAnalysis . CodeActions ;
1213using Microsoft . CodeAnalysis . CodeFixes ;
1617namespace Mud . HttpUtils . CodeFixes ;
1718
1819/// <summary>
19- /// 为 HTTPCLIENT005 诊断提供自动修复:将 URL 模板中的反斜杠 (\) 替换为正斜杠 (/) 。
20+ /// 为 HTTPCLIENT005 诊断提供自动修复:修复 URL 模板中的格式问题 。
2021/// <para>
21- /// HTTPCLIENT005 报告 URL 模板格式无效。常见原因是用户在 URL 中误用反斜杠(如 \api\users)。
22- /// 此 CodeFix 自动将反斜杠替换为正斜杠(如 /api/users)。
22+ /// HTTPCLIENT005 报告 URL 模板格式无效。常见原因包括:
23+ /// <list type="bullet">
24+ /// <item>反斜杠误用(如 \api\users)→ 替换为正斜杠;</item>
25+ /// <item>花括号未配对(如 /api/{id 或 /api/id})→ 补齐或删除错配的花括号。</item>
26+ /// </list>
2327/// </para>
2428/// </summary>
2529[ ExportCodeFixProvider ( LanguageNames . CSharp , Name = nameof ( HttpClientInvalidUrlTemplateCodeFixProvider ) ) ]
2630[ Shared ]
2731public class HttpClientInvalidUrlTemplateCodeFixProvider : CodeFixProvider
2832{
29- private const string FixTitle = "将 URL 反斜杠替换为正斜杠" ;
33+ private const string FixBackslashTitle = "将 URL 反斜杠替换为正斜杠" ;
34+ private const string FixBracesTitle = "修复 URL 模板中的花括号配对" ;
3035
3136 /// <inheritdoc />
3237 public sealed override ImmutableArray < string > FixableDiagnosticIds
@@ -51,7 +56,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
5156 var attribute = token . Parent ? . FirstAncestorOrSelf < AttributeSyntax > ( ) ;
5257 if ( attribute == null ) return ;
5358
54- // 查找包含反斜杠的字符串字面量参数
59+ // 查找 URL 字符串字面量参数
5560 if ( attribute . ArgumentList == null || attribute . ArgumentList . Arguments . Count == 0 )
5661 return ;
5762
@@ -60,15 +65,119 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
6065 return ;
6166
6267 var urlValue = literal . Token . ValueText ;
63- if ( string . IsNullOrEmpty ( urlValue ) || ! urlValue . Contains ( ' \\ ' ) )
68+ if ( string . IsNullOrEmpty ( urlValue ) )
6469 return ;
6570
66- context . RegisterCodeFix (
67- CodeAction . Create (
68- title : FixTitle ,
69- createChangedDocument : c => FixBackslashAsync ( context . Document , attribute , firstArg , urlValue , c ) ,
70- equivalenceKey : $ "{ nameof ( HttpClientInvalidUrlTemplateCodeFixProvider ) } _FixBackslash") ,
71- diagnostic ) ;
71+ // [Phase1 修复 1.3] 移除「实参含反斜杠才注册」的错误门控。
72+ // 只要诊断是 HTTPCLIENT005 即注册,提供与 CSharpCodeValidator 校验维度一致的修复动作。
73+
74+ // 修复动作 1:反斜杠替换为正斜杠(仅当 URL 含反斜杠时提供)
75+ if ( urlValue . Contains ( '\\ ' ) )
76+ {
77+ context . RegisterCodeFix (
78+ CodeAction . Create (
79+ title : FixBackslashTitle ,
80+ createChangedDocument : c => FixBackslashAsync ( context . Document , attribute , firstArg , urlValue , c ) ,
81+ equivalenceKey : $ "{ nameof ( HttpClientInvalidUrlTemplateCodeFixProvider ) } _FixBackslash") ,
82+ diagnostic ) ;
83+ }
84+
85+ // 修复动作 2:花括号配对修复(仅当花括号不匹配时提供)
86+ if ( HasBraceMismatch ( urlValue ) )
87+ {
88+ context . RegisterCodeFix (
89+ CodeAction . Create (
90+ title : FixBracesTitle ,
91+ createChangedDocument : c => FixBracesAsync ( context . Document , attribute , firstArg , urlValue , c ) ,
92+ equivalenceKey : $ "{ nameof ( HttpClientInvalidUrlTemplateCodeFixProvider ) } _FixBraces") ,
93+ diagnostic ) ;
94+ }
95+ }
96+
97+ /// <summary>
98+ /// 检查 URL 模板中花括号是否不匹配。
99+ /// </summary>
100+ private static bool HasBraceMismatch ( string urlValue )
101+ {
102+ int openBraceCount = 0 ;
103+ int closeBraceCount = 0 ;
104+
105+ for ( int i = 0 ; i < urlValue . Length ; i ++ )
106+ {
107+ char c = urlValue [ i ] ;
108+ if ( c == '{' )
109+ {
110+ openBraceCount ++ ;
111+ int endBrace = urlValue . IndexOf ( '}' , i + 1 ) ;
112+ if ( endBrace == - 1 )
113+ return true ; // 未闭合的 {
114+ }
115+ else if ( c == '}' )
116+ {
117+ closeBraceCount ++ ;
118+ if ( closeBraceCount > openBraceCount )
119+ return true ; // 多余的 }
120+ }
121+ }
122+
123+ return openBraceCount != closeBraceCount ;
124+ }
125+
126+ /// <summary>
127+ /// 修复花括号配对:移除多余的右花括号,补齐未闭合的左花括号。
128+ /// </summary>
129+ private static string FixBraceMismatch ( string urlValue )
130+ {
131+ // 策略:逐字符扫描,跟踪花括号配对状态
132+ var result = new StringBuilder ( urlValue . Length ) ;
133+ int openBraceCount = 0 ;
134+ var unmatchedOpenPositions = new List < int > ( ) ;
135+
136+ for ( int i = 0 ; i < urlValue . Length ; i ++ )
137+ {
138+ char c = urlValue [ i ] ;
139+ if ( c == '{' )
140+ {
141+ // 检查是否有对应的 }
142+ int endBrace = urlValue . IndexOf ( '}' , i + 1 ) ;
143+ if ( endBrace == - 1 )
144+ {
145+ // 未闭合的 { — 删除该花括号(将其后的内容保留)
146+ // 跳过这个 {,不写入 result
147+ continue ;
148+ }
149+ openBraceCount ++ ;
150+ unmatchedOpenPositions . Add ( result . Length ) ;
151+ result . Append ( c ) ;
152+ }
153+ else if ( c == '}' )
154+ {
155+ if ( openBraceCount > 0 )
156+ {
157+ openBraceCount -- ;
158+ unmatchedOpenPositions . RemoveAt ( unmatchedOpenPositions . Count - 1 ) ;
159+ result . Append ( c ) ;
160+ }
161+ else
162+ {
163+ // 多余的 } — 删除
164+ // 跳过这个 },不写入 result
165+ continue ;
166+ }
167+ }
168+ else
169+ {
170+ result . Append ( c ) ;
171+ }
172+ }
173+
174+ // 补齐未闭合的 {
175+ foreach ( var pos in unmatchedOpenPositions . OrderByDescending ( x => x ) )
176+ {
177+ result . Insert ( pos + 1 , '}' ) ;
178+ }
179+
180+ return result . ToString ( ) ;
72181 }
73182
74183 private static Task < Document > FixBackslashAsync (
@@ -94,4 +203,26 @@ private static Task<Document> FixBackslashAsync(
94203
95204 return Task . FromResult ( document . WithSyntaxRoot ( newRoot ) ) ;
96205 }
206+
207+ private static Task < Document > FixBracesAsync (
208+ Document document ,
209+ AttributeSyntax attribute ,
210+ AttributeArgumentSyntax arg ,
211+ string originalUrl ,
212+ CancellationToken cancellationToken )
213+ {
214+ var root = document . GetSyntaxRootAsync ( cancellationToken ) . Result ;
215+ if ( root == null ) return Task . FromResult ( document ) ;
216+
217+ var fixedUrl = FixBraceMismatch ( originalUrl ) ;
218+
219+ var newLiteral = SyntaxFactory . LiteralExpression (
220+ SyntaxKind . StringLiteralExpression ,
221+ SyntaxFactory . Literal ( fixedUrl ) ) ;
222+
223+ var newArg = arg . WithExpression ( newLiteral ) ;
224+ var newRoot = root . ReplaceNode ( arg , newArg ) ;
225+
226+ return Task . FromResult ( document . WithSyntaxRoot ( newRoot ) ) ;
227+ }
97228}
0 commit comments