Skip to content

Commit ebfc8a8

Browse files
committed
fix: 修复分析器异常与代码修复位置定位
1 parent 0866a7f commit ebfc8a8

15 files changed

Lines changed: 502 additions & 46 deletions

‎Mud.HttpUtils.CodeFixes/HttpClientInvalidUrlTemplateCodeFixProvider.cs‎

Lines changed: 144 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
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

88
using System.Collections.Immutable;
99
using System.Composition;
10+
using System.Text;
1011
using Microsoft.CodeAnalysis;
1112
using Microsoft.CodeAnalysis.CodeActions;
1213
using Microsoft.CodeAnalysis.CodeFixes;
@@ -16,17 +17,21 @@
1617
namespace 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]
2731
public 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
}

‎Mud.HttpUtils.CodeFixes/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
| `AotJsonContextCodeFixProvider` | `AOT004` / `AOT005` / `AOT006` | DTO / 查询参数类型未被 `JsonSerializerContext` 覆盖(Native AOT 下会漏元数据) | 将缺失类型追加 `[JsonSerializable(typeof(T))]` 到用户可编辑的 `JsonSerializerContext`;若仅存在脚手架生成文件,则新建 `partial` 扩展类 |
1818
| `AotXmlCodeFixProvider` | `AOT007` | Native AOT 上下文下使用 XML 序列化 | 将 `[SerializationMethod(SerializationMethod.Xml)]` 改为 `Json`,确保 AOT 兼容 |
1919
| `HttpClientMutuallyExclusiveCodeFixProvider` | `HTTPCLIENT007` | `[HttpClientApi]` 同时指定 `HttpClient` 与 `TokenManage`(两者互斥) | 提供两个选项:移除 `HttpClient`(保留 `TokenManage`)或移除 `TokenManage`(保留 `HttpClient`) |
20-
| `HttpClientInvalidUrlTemplateCodeFixProvider` | `HTTPCLIENT005` | URL 模板格式无效(常见为误用反斜杠,如 `\api\users`) | 将 URL 中的反斜杠(`\`)自动替换为正斜杠(`/`) |
20+
| `HttpClientInvalidUrlTemplateCodeFixProvider` | `HTTPCLIENT005` | URL 模板格式无效(反斜杠误用或花括号未配对) | 将 URL 中的反斜杠(`\`)自动替换为正斜杠(`/`);修复花括号配对(补齐/删除错配的 `{` `}`) |
2121

2222
## 使用方式
2323

@@ -26,7 +26,7 @@
2626
- `AOT004`/`AOT005`/`AOT006`:选择"将类型添加到 JsonSerializerContext(AOT 兼容)",自动补齐 JSON 源生成上下文。
2727
- `AOT007`:选择"将 XML 序列化改为 JSON(AOT 兼容)"。
2828
- `HTTPCLIENT007`:选择移除 `HttpClient` 或 `TokenManage` 二选一。
29-
- `HTTPCLIENT005`:选择"将 URL 反斜杠替换为正斜杠"。
29+
- `HTTPCLIENT005`:选择"将 URL 反斜杠替换为正斜杠"或"修复 URL 模板中的花括号配对"。
3030

3131
> 修复器与诊断源完全解耦:只要诊断 ID 匹配,即使诊断来自其他扩展也会尝试修复。所有修复器均支持 `Fix All`(批量修复)操作。
3232

‎Mud.HttpUtils.Generator/Analyzers/AotDtoCoverageAnalyzer.cs‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目开发而产生的一切法律纠纷和责任,我们不承担任何责任。
66
// -----------------------------------------------------------------------
77

8+
using System;
89
using System.Collections.Generic;
910
using System.Collections.Immutable;
1011
using System.Linq;
@@ -80,6 +81,20 @@ private static ImmutableDictionary<string, string> TypeProps(INamedTypeSymbol ty
8081
/// <param name="cancellationToken">取消令牌。</param>
8182
/// <returns>诊断集合(无问题或未配置 Context 时为空)。</returns>
8283
public static ImmutableArray<Diagnostic> Analyze(Compilation compilation, CancellationToken cancellationToken)
84+
{
85+
// [Phase2 修复 2.2] 异常护栏:分析器宁少报不可抛,避免 AD0001 整轮禁用。
86+
try
87+
{
88+
return AnalyzeCore(compilation, cancellationToken);
89+
}
90+
catch (Exception ex)
91+
{
92+
GeneratorDebugLogger.LogError(nameof(Analyze), ex);
93+
return ImmutableArray<Diagnostic>.Empty;
94+
}
95+
}
96+
97+
private static ImmutableArray<Diagnostic> AnalyzeCore(Compilation compilation, CancellationToken cancellationToken)
8398
{
8499
var diagnostics = ImmutableArray.CreateBuilder<Diagnostic>();
85100

@@ -150,6 +165,20 @@ public static ImmutableArray<Diagnostic> Analyze(Compilation compilation, Cancel
150165
/// 实体项目应各自运行脚手架生成 internal Context 覆盖自身类型。</para>
151166
/// </remarks>
152167
public static ImmutableArray<Diagnostic> AnalyzeHttpJsonSerializableCoverage(Compilation compilation, CancellationToken cancellationToken)
168+
{
169+
// [Phase2 修复 2.2] 异常护栏:分析器宁少报不可抛,避免 AD0001 整轮禁用。
170+
try
171+
{
172+
return AnalyzeHttpJsonSerializableCoverageCore(compilation, cancellationToken);
173+
}
174+
catch (Exception ex)
175+
{
176+
GeneratorDebugLogger.LogError(nameof(AnalyzeHttpJsonSerializableCoverage), ex);
177+
return ImmutableArray<Diagnostic>.Empty;
178+
}
179+
}
180+
181+
private static ImmutableArray<Diagnostic> AnalyzeHttpJsonSerializableCoverageCore(Compilation compilation, CancellationToken cancellationToken)
153182
{
154183
var diagnostics = ImmutableArray.CreateBuilder<Diagnostic>();
155184

@@ -356,8 +385,10 @@ private static void CheckMethodDtoCoverage(
356385
if (bodyAttr != null && param.GetAttributes()
357386
.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, bodyAttr)))
358387
{
359-
// FormUrlEncoded Body 不走 JSON 序列化,无需 JsonSerializerContext 覆盖,跳过 AOT004 检查。
360-
if (GetMethodSerializationMethod(method) == "FormUrlEncoded")
388+
// FormUrlEncoded / Xml Body 不走 JSON 序列化,无需 JsonSerializerContext 覆盖,跳过 AOT004 检查。
389+
// [Phase2 修复 2.5] 增加 Xml 豁免,防止 XML 方法被 AOT004 误报。
390+
var ser = GetMethodSerializationMethod(method);
391+
if (ser is "FormUrlEncoded" or "Xml")
361392
continue;
362393

363394
if (!IsCovered(paramType, coveredTypes) && !QuerySerializationClassifier.IsSimple(paramType))
@@ -445,10 +476,15 @@ private static void CheckMethodDtoCoverage(
445476

446477
// 2) 非 JSON 契约的 Task<T> 返回(生成器不走反序列化)→ 跳过:
447478
// HttpResponseMessage(SendRawAsync 直达)、Stream/byte[](下载分支)、简单类型。
479+
// [Phase2 修复 2.5] XML 序列化的响应不走 JSON 反序列化,也跳过,防止 AOT004 误报。
448480
if (IsHttpResponseMessage(responseType) || IsStream(responseType) ||
449481
IsByteArray(responseType) || QuerySerializationClassifier.IsSimple(responseType))
450482
return;
451483

484+
// XML 序列化方法豁免:响应端用 XML 反序列化,不需要 JsonSerializerContext 覆盖。
485+
if (GetMethodSerializationMethod(method) is "Xml")
486+
return;
487+
452488
// 3) 其余才做覆盖判定
453489
if (!IsCovered(responseType, coveredTypes) && !QuerySerializationClassifier.IsSimple(responseType))
454490
{

‎Mud.HttpUtils.Generator/Analyzers/HttpJsonSerializableCoverageAnalyzer.cs‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目开发而产生的一切法律纠纷和责任,我们不承担任何责任!
66
// -----------------------------------------------------------------------
77

8+
using System;
89
using System.Collections.Immutable;
910
using Microsoft.CodeAnalysis;
1011
using Microsoft.CodeAnalysis.Diagnostics;
@@ -34,10 +35,18 @@ public override void Initialize(AnalysisContext context)
3435
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
3536
context.RegisterCompilationAction(static ctx =>
3637
{
37-
foreach (var diagnostic in AotDtoCoverageAnalyzer
38-
.AnalyzeHttpJsonSerializableCoverage(ctx.Compilation, ctx.CancellationToken))
38+
// [Phase2 修复 2.2] 异常护栏:分析器宁少报不可抛,避免 AD0001 整轮禁用。
39+
try
3940
{
40-
ctx.ReportDiagnostic(diagnostic);
41+
foreach (var diagnostic in AotDtoCoverageAnalyzer
42+
.AnalyzeHttpJsonSerializableCoverage(ctx.Compilation, ctx.CancellationToken))
43+
{
44+
ctx.ReportDiagnostic(diagnostic);
45+
}
46+
}
47+
catch (Exception ex)
48+
{
49+
GeneratorDebugLogger.LogError(nameof(HttpJsonSerializableCoverageAnalyzer), ex);
4150
}
4251
});
4352
}

0 commit comments

Comments
 (0)