diff --git a/src/Generators/Microsoft.Gen.AutoClient/Parser.cs b/src/Generators/Microsoft.Gen.AutoClient/Parser.cs index 81e9cb501d3..818c724f396 100644 --- a/src/Generators/Microsoft.Gen.AutoClient/Parser.cs +++ b/src/Generators/Microsoft.Gen.AutoClient/Parser.cs @@ -318,11 +318,13 @@ potentialNamespaceParent is not NamespaceDeclarationSyntax && return result; } - private static ParseMethodAttributesResult ParseMethodAttributes(ImmutableArray methodAttributes, SymbolHolder symbols) + private ParseMethodAttributesResult ParseMethodAttributes(ImmutableArray methodAttributes, SymbolHolder symbols) { List httpMethods = []; string? requestName = null; string? path = null; + bool requestNameFailed = false; + bool headersParsingFailed = false; Dictionary staticHeaders = []; foreach (var methodAttribute in methodAttributes) @@ -342,7 +344,19 @@ private static ParseMethodAttributesResult ParseMethodAttributes(ImmutableArray< continue; } - path = methodAttribute.ConstructorArguments[0].Value as string; + var methodArg = methodAttribute.ConstructorArguments[0]; + if (methodArg.IsNull) + { + continue; + } + + var argString = methodArg.Value as string; + if (string.IsNullOrEmpty(argString)) + { + continue; + } + + path = argString; } else if (attributeSymbol.Equals(symbols.RestPostAttribute, SymbolEqualityComparer.Default)) { @@ -353,7 +367,19 @@ private static ParseMethodAttributesResult ParseMethodAttributes(ImmutableArray< continue; } - path = methodAttribute.ConstructorArguments[0].Value as string; + var methodArg = methodAttribute.ConstructorArguments[0]; + if (methodArg.IsNull) + { + continue; + } + + var argString = methodArg.Value as string; + if (string.IsNullOrEmpty(argString)) + { + continue; + } + + path = argString; } else if (attributeSymbol.Equals(symbols.RestPutAttribute, SymbolEqualityComparer.Default)) { @@ -364,7 +390,19 @@ private static ParseMethodAttributesResult ParseMethodAttributes(ImmutableArray< continue; } - path = methodAttribute.ConstructorArguments[0].Value as string; + var methodArg = methodAttribute.ConstructorArguments[0]; + if (methodArg.IsNull) + { + continue; + } + + var argString = methodArg.Value as string; + if (string.IsNullOrEmpty(argString)) + { + continue; + } + + path = argString; } else if (attributeSymbol.Equals(symbols.RestDeleteAttribute, SymbolEqualityComparer.Default)) { @@ -375,7 +413,19 @@ private static ParseMethodAttributesResult ParseMethodAttributes(ImmutableArray< continue; } - path = methodAttribute.ConstructorArguments[0].Value as string; + var methodArg = methodAttribute.ConstructorArguments[0]; + if (methodArg.IsNull) + { + continue; + } + + var argString = methodArg.Value as string; + if (string.IsNullOrEmpty(argString)) + { + continue; + } + + path = argString; } else if (attributeSymbol.Equals(symbols.RestPatchAttribute, SymbolEqualityComparer.Default)) { @@ -386,7 +436,19 @@ private static ParseMethodAttributesResult ParseMethodAttributes(ImmutableArray< continue; } - path = methodAttribute.ConstructorArguments[0].Value as string; + var methodArg = methodAttribute.ConstructorArguments[0]; + if (methodArg.IsNull) + { + continue; + } + + var argString = methodArg.Value as string; + if (string.IsNullOrEmpty(argString)) + { + continue; + } + + path = argString; } else if (attributeSymbol.Equals(symbols.RestOptionsAttribute, SymbolEqualityComparer.Default)) { @@ -397,7 +459,19 @@ private static ParseMethodAttributesResult ParseMethodAttributes(ImmutableArray< continue; } - path = methodAttribute.ConstructorArguments[0].Value as string; + var methodArg = methodAttribute.ConstructorArguments[0]; + if (methodArg.IsNull) + { + continue; + } + + var argString = methodArg.Value as string; + if (string.IsNullOrEmpty(argString)) + { + continue; + } + + path = argString; } else if (attributeSymbol.Equals(symbols.RestHeadAttribute, SymbolEqualityComparer.Default)) { @@ -408,7 +482,19 @@ private static ParseMethodAttributesResult ParseMethodAttributes(ImmutableArray< continue; } - path = methodAttribute.ConstructorArguments[0].Value as string; + var methodArg = methodAttribute.ConstructorArguments[0]; + if (methodArg.IsNull) + { + continue; + } + + var argString = methodArg.Value as string; + if (string.IsNullOrEmpty(argString)) + { + continue; + } + + path = argString; } else if (attributeSymbol.Equals(symbols.RestStaticHeaderAttribute, SymbolEqualityComparer.Default)) { @@ -417,28 +503,67 @@ private static ParseMethodAttributesResult ParseMethodAttributes(ImmutableArray< continue; } - var key = methodAttribute.ConstructorArguments[0].Value as string; - var value = methodAttribute.ConstructorArguments[1].Value as string; + var keyArg = methodAttribute.ConstructorArguments[0]; + var valueArg = methodAttribute.ConstructorArguments[1]; - if (key == null || value == null) + if (keyArg.IsNull) { + Diag(DiagDescriptors.ErrorInvalidHeaderName, attributeSymbol.GetLocation()); + headersParsingFailed = true; continue; } - staticHeaders.Add(key, value); + if (valueArg.IsNull) + { + Diag(DiagDescriptors.ErrorInvalidHeaderValue, attributeSymbol.GetLocation()); + headersParsingFailed = true; + continue; + } + + var keyString = keyArg.Value as string; + var valueString = valueArg.Value as string; + + if (string.IsNullOrEmpty(keyString)) + { + Diag(DiagDescriptors.ErrorInvalidHeaderName, attributeSymbol.GetLocation()); + headersParsingFailed = true; + continue; + } + + if (valueString == null) + { + Diag(DiagDescriptors.ErrorInvalidHeaderValue, attributeSymbol.GetLocation()); + headersParsingFailed = true; + continue; + } + + staticHeaders.Add(keyString!, valueString); } foreach (var a in methodAttribute.NamedArguments) { if (a.Key == RequestNamePropertyName) { - requestName = (string)a.Value.Value!; + if (a.Value.IsNull) + { + requestNameFailed = true; + continue; + } + + var valueString = a.Value.Value as string; + if (string.IsNullOrEmpty(valueString)) + { + requestNameFailed = true; + continue; + } + + requestName = valueString; break; } } } - return new(httpMethods, path, requestName, staticHeaders); + return new(httpMethods, path, requestName, requestNameFailed, headersParsingFailed, staticHeaders); } private RestApiMethod? ProcessMethod( @@ -474,6 +599,18 @@ private static ParseMethodAttributesResult ParseMethodAttributes(ImmutableArray< hasErrors = true; } + if (methodAttrResult.RequestNameParsingFailed) + { + Diag(DiagDescriptors.ErrorInvalidRequestName, methodSymbol.GetLocation()); + hasErrors = true; + } + + if (methodAttrResult.HeadersParsingFailed) + { + // Diagnostics are already emitted from the parsing method + hasErrors = true; + } + var returnTypeSymbol = (INamedTypeSymbol)methodSymbol.ReturnType; ITypeSymbol? innerType = null; @@ -662,5 +799,7 @@ private sealed record class ParseMethodAttributesResult( List HttpMethods, string? Path, string? RequestName, + bool RequestNameParsingFailed, + bool HeadersParsingFailed, Dictionary StaticHeaders); } diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/DeleteAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/DeleteAttribute.cs index fb7bb255e28..4c8e8b169ee 100644 --- a/src/Libraries/Microsoft.Extensions.Http.AutoClient/DeleteAttribute.cs +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/DeleteAttribute.cs @@ -34,7 +34,7 @@ public sealed class DeleteAttribute : Attribute /// /// Initializes a new instance of the class. /// - /// The path of the request. + /// The path of the request. Cannot be empty or null. public DeleteAttribute(string path) { Path = path; diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/GetAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/GetAttribute.cs index f7a3b329d6b..de16b0226f2 100644 --- a/src/Libraries/Microsoft.Extensions.Http.AutoClient/GetAttribute.cs +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/GetAttribute.cs @@ -34,7 +34,7 @@ public sealed class GetAttribute : Attribute /// /// Initializes a new instance of the class. /// - /// The path of the request. + /// The path of the request. Cannot be empty or null. public GetAttribute(string path) { Path = path; diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/HeadAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/HeadAttribute.cs index 24d06d3c423..8a31fdb0d2b 100644 --- a/src/Libraries/Microsoft.Extensions.Http.AutoClient/HeadAttribute.cs +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/HeadAttribute.cs @@ -34,7 +34,7 @@ public sealed class HeadAttribute : Attribute /// /// Initializes a new instance of the class. /// - /// The path of the request. + /// The path of the request. Cannot be empty or null. public HeadAttribute(string path) { Path = path; diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/OptionsAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/OptionsAttribute.cs index 9462d85c6f8..079a823c4ba 100644 --- a/src/Libraries/Microsoft.Extensions.Http.AutoClient/OptionsAttribute.cs +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/OptionsAttribute.cs @@ -34,7 +34,7 @@ public sealed class OptionsAttribute : Attribute /// /// Initializes a new instance of the class. /// - /// The path of the request. + /// The path of the request. Cannot be empty or null. public OptionsAttribute(string path) { Path = path; diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/PatchAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/PatchAttribute.cs index ea307c58dff..08eb10ba8d0 100644 --- a/src/Libraries/Microsoft.Extensions.Http.AutoClient/PatchAttribute.cs +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/PatchAttribute.cs @@ -34,7 +34,7 @@ public sealed class PatchAttribute : Attribute /// /// Initializes a new instance of the class. /// - /// The path of the request. + /// The path of the request. Cannot be empty or null. public PatchAttribute(string path) { Path = path; diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/PostAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/PostAttribute.cs index 0596107ec6a..eb4d529472d 100644 --- a/src/Libraries/Microsoft.Extensions.Http.AutoClient/PostAttribute.cs +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/PostAttribute.cs @@ -34,7 +34,7 @@ public sealed class PostAttribute : Attribute /// /// Initializes a new instance of the class. /// - /// The path of the request. + /// The path of the request. Cannot be empty or null. public PostAttribute(string path) { Path = path; diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/PutAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/PutAttribute.cs index ca37d9509ad..03bad7896fd 100644 --- a/src/Libraries/Microsoft.Extensions.Http.AutoClient/PutAttribute.cs +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/PutAttribute.cs @@ -34,7 +34,7 @@ public sealed class PutAttribute : Attribute /// /// Initializes a new instance of the class. /// - /// The path of the request. + /// The path of the request. Cannot be empty or null. public PutAttribute(string path) { Path = path; diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/StaticHeaderAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/StaticHeaderAttribute.cs index 8fd921809a4..657dfd1bbfe 100644 --- a/src/Libraries/Microsoft.Extensions.Http.AutoClient/StaticHeaderAttribute.cs +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/StaticHeaderAttribute.cs @@ -13,6 +13,7 @@ namespace Microsoft.Extensions.Http.AutoClient; /// Injects a static header to be sent with every request. When this attribute is applied /// to an interface, then it impacts every method described by the interface. Otherwise, it only /// affects the method where it is applied. +/// The header name must not be null or empty. The value, on the other hand, can be empty, but not null. /// /// /// @@ -33,8 +34,8 @@ public sealed class StaticHeaderAttribute : Attribute /// /// Initializes a new instance of the class. /// - /// The name of the header. - /// The value of the header. + /// The name of the header. Cannot be empty or null. + /// The value of the header. Cannot be null. public StaticHeaderAttribute(string header, string value) { Header = header; diff --git a/test/Generators/Microsoft.Gen.AutoClient/Unit/ParserTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Unit/ParserTests.cs index 4bf2333058e..a3872033ac2 100644 --- a/test/Generators/Microsoft.Gen.AutoClient/Unit/ParserTests.cs +++ b/test/Generators/Microsoft.Gen.AutoClient/Unit/ParserTests.cs @@ -543,6 +543,122 @@ public interface IClient } } + [Fact] + public async Task NullPath() + { + var ds = await RunGenerator(@$" + [AutoClient(""MyClient"")] + public interface IClient + {{ + [Get(null!)] + public Task GetUsers(CancellationToken token); + }}"); + + Assert.Contains(DiagDescriptors.ErrorMissingMethodAttribute.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task EmptyPath() + { + var ds = await RunGenerator(@$" + [AutoClient(""MyClient"")] + public interface IClient + {{ + [Get("""")] + public Task GetUsers(CancellationToken token); + }}"); + + Assert.Contains(DiagDescriptors.ErrorMissingMethodAttribute.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task NullStaticHeaderKey() + { + var ds = await RunGenerator(@$" + [AutoClient(""MyClient"")] + public interface IClient + {{ + [Get(""/api/users"")] + [StaticHeader(null!, ""value"")] + public Task GetUsers(CancellationToken token); + }}"); + + Assert.Contains(DiagDescriptors.ErrorInvalidHeaderName.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task NullStaticHeaderValue() + { + var ds = await RunGenerator(@$" + [AutoClient(""MyClient"")] + public interface IClient + {{ + [Get(""/api/users"")] + [StaticHeader(""key"", null!)] + public Task GetUsers(CancellationToken token); + }}"); + + Assert.Contains(DiagDescriptors.ErrorInvalidHeaderValue.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task EmptyStaticHeaderKey() + { + var ds = await RunGenerator(@$" + [AutoClient(""MyClient"")] + public interface IClient + {{ + [Get(""/api/users"")] + [StaticHeader("""", ""value"")] + public Task GetUsers(CancellationToken token); + }}"); + + Assert.Contains(DiagDescriptors.ErrorInvalidHeaderName.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task EmptyStaticHeaderValue_NoError() + { + var ds = await RunGenerator(@$" + [AutoClient(""MyClient"")] + public interface IClient + {{ + [Get(""/api/users"")] + [StaticHeader(""key"", """")] + public Task GetUsers(CancellationToken token); + }}"); + + Assert.Empty(ds); + } + + [Fact] + public async Task NullRequestName() + { + var ds = await RunGenerator(@$" + [AutoClient(""MyClient"")] + public interface IClient + {{ + [Get(""/api/users"", RequestName = null!)] + public Task GetUsers(CancellationToken token); + }}"); + + Assert.Contains(DiagDescriptors.ErrorInvalidRequestName.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task EmptyRequestName() + { + var ds = await RunGenerator(@$" + [AutoClient(""MyClient"")] + public interface IClient + {{ + [Get(""/api/users"", RequestName = """")] + public Task GetUsers(CancellationToken token); + }}"); + + Assert.Contains(DiagDescriptors.ErrorInvalidRequestName.Id, ds.Select(x => x.Id)); + } + [Fact] public async Task InvalidRequestName() {