| | | 1 | | using System.Collections.Concurrent; |
| | | 2 | | using System.Text.RegularExpressions; |
| | | 3 | | using Microsoft.CodeAnalysis; |
| | | 4 | | using Microsoft.CodeAnalysis.Diagnostics; |
| | | 5 | | |
| | | 6 | | namespace TeleFlow.Generators; |
| | | 7 | | |
| | | 8 | | public sealed partial class TelegramHandlerAnalyzer |
| | | 9 | | { |
| | | 10 | | private const int CommandPrefixModeRequired = 0; |
| | | 11 | | private const int CommandPrefixModeOptional = 1; |
| | | 12 | | private const int CommandPrefixModeNoPrefix = 2; |
| | | 13 | | |
| | | 14 | | private static HandlerKind? GetRouteKind( |
| | | 15 | | IMethodSymbol method, |
| | | 16 | | bool includeClassRouteAttributes, |
| | | 17 | | out ITypeSymbol? callbackPayloadType, |
| | | 18 | | out bool hasRawCallback, |
| | | 19 | | out bool hasMixedCallbackRoutes) |
| | | 20 | | { |
| | 97 | 21 | | callbackPayloadType = null; |
| | 97 | 22 | | hasRawCallback = GetRouteAttributes( |
| | 97 | 23 | | method, |
| | 97 | 24 | | TelegramHandlerSymbols.CallbackAttribute, |
| | 97 | 25 | | includeClassRouteAttributes).Count > 0; |
| | | 26 | | |
| | 97 | 27 | | IReadOnlyList<AttributeData> genericCallbackAttributes = GetGenericRouteAttributes( |
| | 97 | 28 | | method, |
| | 97 | 29 | | TelegramHandlerSymbols.GenericCallbackAttribute, |
| | 97 | 30 | | includeClassRouteAttributes); |
| | 97 | 31 | | AttributeData? genericCallback = genericCallbackAttributes.Count > 0 |
| | 97 | 32 | | ? genericCallbackAttributes[0] |
| | 97 | 33 | | : null; |
| | | 34 | | |
| | 97 | 35 | | if (genericCallback?.AttributeClass is INamedTypeSymbol { TypeArguments.Length: 1 } genericCallbackType) |
| | | 36 | | { |
| | 9 | 37 | | callbackPayloadType = genericCallbackType.TypeArguments[0]; |
| | | 38 | | } |
| | | 39 | | |
| | 97 | 40 | | bool hasCallback = hasRawCallback || callbackPayloadType is not null; |
| | 97 | 41 | | int messageRouteCount = GetMessageRouteAttributeCount(method, includeClassRouteAttributes); |
| | 97 | 42 | | int chatMemberRouteCount = GetChatMemberRouteAttributeCount(method, includeClassRouteAttributes); |
| | 97 | 43 | | hasMixedCallbackRoutes = (hasRawCallback && callbackPayloadType is not null) || |
| | 97 | 44 | | (hasCallback && (messageRouteCount > 0 || chatMemberRouteCount > 0)) || |
| | 97 | 45 | | (chatMemberRouteCount > 0 && messageRouteCount > 0); |
| | | 46 | | |
| | 97 | 47 | | if (hasMixedCallbackRoutes) |
| | | 48 | | { |
| | 3 | 49 | | return null; |
| | | 50 | | } |
| | | 51 | | |
| | 94 | 52 | | if (hasCallback) |
| | | 53 | | { |
| | 18 | 54 | | return HandlerKind.Callback; |
| | | 55 | | } |
| | | 56 | | |
| | 76 | 57 | | if (chatMemberRouteCount > 0) |
| | | 58 | | { |
| | 11 | 59 | | return HandlerKind.ChatMember; |
| | | 60 | | } |
| | | 61 | | |
| | 65 | 62 | | return messageRouteCount > 0 ? HandlerKind.Message : null; |
| | | 63 | | } |
| | | 64 | | |
| | | 65 | | private static int GetMessageRouteAttributeCount( |
| | | 66 | | IMethodSymbol method, |
| | | 67 | | bool includeClassRouteAttributes = false) |
| | | 68 | | { |
| | 119 | 69 | | return GetRouteAttributes(method, TelegramHandlerSymbols.CommandAttribute, includeClassRouteAttributes).Count + |
| | 119 | 70 | | GetRouteAttributes(method, TelegramHandlerSymbols.MessageAttribute, includeClassRouteAttributes).Count + |
| | 119 | 71 | | GetRouteAttributes(method, TelegramHandlerSymbols.TextAttribute, includeClassRouteAttributes).Count + |
| | 119 | 72 | | GetRouteAttributes(method, TelegramHandlerSymbols.TextTemplateAttribute, includeClassRouteAttributes).Cou |
| | 119 | 73 | | GetRouteAttributes(method, TelegramHandlerSymbols.CommandTemplateAttribute, includeClassRouteAttributes). |
| | 119 | 74 | | GetRouteAttributes(method, TelegramHandlerSymbols.TextRegexAttribute, includeClassRouteAttributes).Count |
| | 119 | 75 | | GetRouteAttributes(method, TelegramHandlerSymbols.CommandRegexAttribute, includeClassRouteAttributes).Cou |
| | | 76 | | } |
| | | 77 | | |
| | | 78 | | private static bool HasAnyMessageRouteAttribute( |
| | | 79 | | IMethodSymbol method, |
| | | 80 | | bool includeClassRouteAttributes = false) |
| | | 81 | | { |
| | 16 | 82 | | return GetMessageRouteAttributeCount(method, includeClassRouteAttributes) > 0; |
| | | 83 | | } |
| | | 84 | | |
| | | 85 | | private static int GetChatMemberRouteAttributeCount( |
| | | 86 | | IMethodSymbol method, |
| | | 87 | | bool includeClassRouteAttributes = false) |
| | | 88 | | { |
| | 97 | 89 | | return GetRouteAttributes(method, TelegramHandlerSymbols.ChatMemberUpdatedAttribute, includeClassRouteAttributes |
| | 97 | 90 | | GetRouteAttributes(method, TelegramHandlerSymbols.MyChatMemberUpdatedAttribute, includeClassRouteAttribut |
| | | 91 | | } |
| | | 92 | | |
| | | 93 | | private static void AnalyzeCommandRoutes( |
| | | 94 | | SymbolAnalysisContext context, |
| | | 95 | | IMethodSymbol method, |
| | | 96 | | ConcurrentBag<CommandRegistration> commands, |
| | | 97 | | string displayName, |
| | | 98 | | Location? location, |
| | | 99 | | bool includeClassRouteAttributes = false) |
| | | 100 | | { |
| | 184 | 101 | | foreach (AttributeData attribute in GetRouteAttributes( |
| | 86 | 102 | | method, |
| | 86 | 103 | | TelegramHandlerSymbols.CommandAttribute, |
| | 86 | 104 | | includeClassRouteAttributes)) |
| | | 105 | | { |
| | 6 | 106 | | string? command = GetConstructorString(attribute); |
| | | 107 | | |
| | 6 | 108 | | if (!IsValidCommand(command)) |
| | | 109 | | { |
| | 1 | 110 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 111 | | InvalidCommand, |
| | 1 | 112 | | location, |
| | 1 | 113 | | displayName)); |
| | 1 | 114 | | continue; |
| | | 115 | | } |
| | | 116 | | |
| | 5 | 117 | | if (!TryGetPrefixes(attribute, out IReadOnlyList<string> prefixes, out string prefixReason)) |
| | | 118 | | { |
| | 1 | 119 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 120 | | InvalidCommandPrefix, |
| | 1 | 121 | | location, |
| | 1 | 122 | | prefixReason)); |
| | 1 | 123 | | continue; |
| | | 124 | | } |
| | | 125 | | |
| | 4 | 126 | | int prefixMode = GetCommandPrefixMode(attribute); |
| | | 127 | | |
| | 4 | 128 | | if (!TryValidateCommandPrefixMode(attribute, prefixMode, out string prefixModeReason)) |
| | | 129 | | { |
| | 0 | 130 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 0 | 131 | | InvalidCommandPrefix, |
| | 0 | 132 | | location, |
| | 0 | 133 | | prefixModeReason)); |
| | 0 | 134 | | continue; |
| | | 135 | | } |
| | | 136 | | |
| | 4 | 137 | | bool allowSpace = GetNamedBool(attribute, "AllowSpaceAfterPrefix", defaultValue: false); |
| | | 138 | | |
| | 4 | 139 | | if (prefixMode is CommandPrefixModeRequired or CommandPrefixModeOptional) |
| | | 140 | | { |
| | 16 | 141 | | foreach (string prefix in prefixes) |
| | | 142 | | { |
| | 4 | 143 | | commands.Add(new CommandRegistration( |
| | 4 | 144 | | $"{prefix.ToUpperInvariant()}\u001F{allowSpace}\u001FPREFIXED\u001F{command!.ToUpperInvariant()} |
| | 4 | 145 | | $"{prefix}{command}", |
| | 4 | 146 | | location)); |
| | | 147 | | } |
| | | 148 | | } |
| | | 149 | | |
| | 4 | 150 | | if (prefixMode is CommandPrefixModeOptional or CommandPrefixModeNoPrefix) |
| | | 151 | | { |
| | 0 | 152 | | commands.Add(new CommandRegistration( |
| | 0 | 153 | | $"NO_PREFIX\u001F{command!.ToUpperInvariant()}", |
| | 0 | 154 | | command, |
| | 0 | 155 | | location)); |
| | | 156 | | } |
| | | 157 | | } |
| | 86 | 158 | | } |
| | | 159 | | |
| | | 160 | | private static void AnalyzeTemplateAndRegexRoutes( |
| | | 161 | | SymbolAnalysisContext context, |
| | | 162 | | IMethodSymbol method, |
| | | 163 | | HandlerKind routeKind, |
| | | 164 | | Location? location, |
| | | 165 | | bool includeClassRouteAttributes = false) |
| | | 166 | | { |
| | 86 | 167 | | if (routeKind is HandlerKind.Callback or HandlerKind.ChatMember) |
| | | 168 | | { |
| | 24 | 169 | | return; |
| | | 170 | | } |
| | | 171 | | |
| | 62 | 172 | | List<IReadOnlyDictionary<string, RouteValueDefinition>> routeValues = new List<IReadOnlyDictionary<string, Route |
| | | 173 | | |
| | 142 | 174 | | foreach (AttributeData attribute in GetRouteAttributes( |
| | 62 | 175 | | method, |
| | 62 | 176 | | TelegramHandlerSymbols.TextTemplateAttribute, |
| | 62 | 177 | | includeClassRouteAttributes)) |
| | | 178 | | { |
| | 9 | 179 | | string? template = GetConstructorString(attribute); |
| | | 180 | | |
| | 9 | 181 | | if (!TryParseRouteTemplate(template, out IReadOnlyDictionary<string, RouteValueDefinition> values, out strin |
| | | 182 | | { |
| | 4 | 183 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 4 | 184 | | InvalidRouteTemplate, |
| | 4 | 185 | | location, |
| | 4 | 186 | | reason)); |
| | 4 | 187 | | continue; |
| | | 188 | | } |
| | | 189 | | |
| | 5 | 190 | | routeValues.Add(values); |
| | | 191 | | } |
| | | 192 | | |
| | 130 | 193 | | foreach (AttributeData attribute in GetRouteAttributes( |
| | 62 | 194 | | method, |
| | 62 | 195 | | TelegramHandlerSymbols.CommandTemplateAttribute, |
| | 62 | 196 | | includeClassRouteAttributes)) |
| | | 197 | | { |
| | 3 | 198 | | string? template = GetConstructorString(attribute); |
| | | 199 | | |
| | 3 | 200 | | if (!TryParseRouteTemplate(template, out IReadOnlyDictionary<string, RouteValueDefinition> values, out strin |
| | | 201 | | { |
| | 0 | 202 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 0 | 203 | | InvalidRouteTemplate, |
| | 0 | 204 | | location, |
| | 0 | 205 | | reason)); |
| | 0 | 206 | | continue; |
| | | 207 | | } |
| | | 208 | | |
| | 3 | 209 | | if (!TryGetPrefixes(attribute, out IReadOnlyList<string> prefixes, out string prefixReason)) |
| | | 210 | | { |
| | 0 | 211 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 0 | 212 | | InvalidCommandPrefix, |
| | 0 | 213 | | location, |
| | 0 | 214 | | prefixReason)); |
| | 0 | 215 | | continue; |
| | | 216 | | } |
| | | 217 | | |
| | 3 | 218 | | int prefixMode = GetCommandPrefixMode(attribute); |
| | | 219 | | |
| | 3 | 220 | | if (!TryValidateCommandPrefixMode(attribute, prefixMode, out string prefixModeReason)) |
| | | 221 | | { |
| | 1 | 222 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 223 | | InvalidCommandPrefix, |
| | 1 | 224 | | location, |
| | 1 | 225 | | prefixModeReason)); |
| | 1 | 226 | | continue; |
| | | 227 | | } |
| | | 228 | | |
| | 2 | 229 | | if (CommandPatternStartsWithPrefix(template!, prefixes)) |
| | | 230 | | { |
| | 2 | 231 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 2 | 232 | | InvalidRouteTemplate, |
| | 2 | 233 | | location, |
| | 2 | 234 | | "Command route template value must not include a command prefix.")); |
| | 2 | 235 | | continue; |
| | | 236 | | } |
| | | 237 | | |
| | 0 | 238 | | routeValues.Add(values); |
| | | 239 | | } |
| | | 240 | | |
| | 128 | 241 | | foreach (AttributeData attribute in GetRouteAttributes( |
| | 62 | 242 | | method, |
| | 62 | 243 | | TelegramHandlerSymbols.TextRegexAttribute, |
| | 62 | 244 | | includeClassRouteAttributes)) |
| | | 245 | | { |
| | 2 | 246 | | string? pattern = GetConstructorString(attribute); |
| | | 247 | | |
| | 2 | 248 | | if (!TryParseRouteRegex(pattern, out IReadOnlyDictionary<string, RouteValueDefinition> values, out string re |
| | | 249 | | { |
| | 1 | 250 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 251 | | InvalidRouteRegex, |
| | 1 | 252 | | location, |
| | 1 | 253 | | reason)); |
| | 1 | 254 | | continue; |
| | | 255 | | } |
| | | 256 | | |
| | 1 | 257 | | routeValues.Add(values); |
| | | 258 | | } |
| | | 259 | | |
| | 126 | 260 | | foreach (AttributeData attribute in GetRouteAttributes( |
| | 62 | 261 | | method, |
| | 62 | 262 | | TelegramHandlerSymbols.CommandRegexAttribute, |
| | 62 | 263 | | includeClassRouteAttributes)) |
| | | 264 | | { |
| | 1 | 265 | | string? pattern = GetConstructorString(attribute); |
| | | 266 | | |
| | 1 | 267 | | if (!TryParseRouteRegex(pattern, out IReadOnlyDictionary<string, RouteValueDefinition> values, out string re |
| | | 268 | | { |
| | 0 | 269 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 0 | 270 | | InvalidRouteRegex, |
| | 0 | 271 | | location, |
| | 0 | 272 | | reason)); |
| | 0 | 273 | | continue; |
| | | 274 | | } |
| | | 275 | | |
| | 1 | 276 | | if (!TryGetPrefixes(attribute, out _, out string prefixReason)) |
| | | 277 | | { |
| | 1 | 278 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 279 | | InvalidCommandPrefix, |
| | 1 | 280 | | location, |
| | 1 | 281 | | prefixReason)); |
| | 1 | 282 | | continue; |
| | | 283 | | } |
| | | 284 | | |
| | 0 | 285 | | int prefixMode = GetCommandPrefixMode(attribute); |
| | | 286 | | |
| | 0 | 287 | | if (!TryValidateCommandPrefixMode(attribute, prefixMode, out string prefixModeReason)) |
| | | 288 | | { |
| | 0 | 289 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 0 | 290 | | InvalidCommandPrefix, |
| | 0 | 291 | | location, |
| | 0 | 292 | | prefixModeReason)); |
| | 0 | 293 | | continue; |
| | | 294 | | } |
| | | 295 | | |
| | 0 | 296 | | routeValues.Add(values); |
| | | 297 | | } |
| | | 298 | | |
| | 62 | 299 | | ValidateRouteValueParameters(context, method, routeValues, location, includeClassRouteAttributes); |
| | 62 | 300 | | } |
| | | 301 | | |
| | | 302 | | private static void ValidateRouteValueParameters( |
| | | 303 | | SymbolAnalysisContext context, |
| | | 304 | | IMethodSymbol method, |
| | | 305 | | IReadOnlyList<IReadOnlyDictionary<string, RouteValueDefinition>> routeValues, |
| | | 306 | | Location? location, |
| | | 307 | | bool includeClassRouteAttributes = false) |
| | | 308 | | { |
| | 62 | 309 | | if (routeValues.Count == 0) |
| | | 310 | | { |
| | 56 | 311 | | return; |
| | | 312 | | } |
| | | 313 | | |
| | 6 | 314 | | string[] firstNames = routeValues[0].Keys.OrderBy(static name => name, StringComparer.Ordinal).ToArray(); |
| | | 315 | | |
| | 6 | 316 | | if (GetMessageRouteAttributeCount(method, includeClassRouteAttributes) > routeValues.Count) |
| | | 317 | | { |
| | 0 | 318 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 0 | 319 | | InvalidRouteValue, |
| | 0 | 320 | | location, |
| | 0 | 321 | | "A handler method with route value parameters cannot also have routes that do not provide route values." |
| | 0 | 322 | | return; |
| | | 323 | | } |
| | | 324 | | |
| | 12 | 325 | | foreach (IReadOnlyDictionary<string, RouteValueDefinition> values in routeValues.Skip(1)) |
| | | 326 | | { |
| | 0 | 327 | | string[] names = values.Keys.OrderBy(static name => name, StringComparer.Ordinal).ToArray(); |
| | | 328 | | |
| | 0 | 329 | | if (!firstNames.SequenceEqual(names, StringComparer.Ordinal)) |
| | | 330 | | { |
| | 0 | 331 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 0 | 332 | | InvalidRouteValue, |
| | 0 | 333 | | location, |
| | 0 | 334 | | "Multiple route attributes on one handler method must expose the same route value names.")); |
| | 0 | 335 | | return; |
| | | 336 | | } |
| | | 337 | | } |
| | | 338 | | |
| | 24 | 339 | | foreach (string name in firstNames) |
| | | 340 | | { |
| | 16 | 341 | | IParameterSymbol? parameter = method.Parameters.FirstOrDefault(parameter => string.Equals(parameter.Name, na |
| | | 342 | | |
| | 6 | 343 | | if (parameter is null) |
| | | 344 | | { |
| | 2 | 345 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 2 | 346 | | InvalidRouteValue, |
| | 2 | 347 | | location, |
| | 2 | 348 | | $"Route value '{name}' must have a matching handler parameter with the same name.")); |
| | 2 | 349 | | continue; |
| | | 350 | | } |
| | | 351 | | |
| | 4 | 352 | | if (!IsSupportedRouteValueParameterType(parameter.Type)) |
| | | 353 | | { |
| | 1 | 354 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 355 | | InvalidRouteValue, |
| | 1 | 356 | | parameter.Locations.FirstOrDefault(static candidate => candidate.IsInSource) ?? location, |
| | 1 | 357 | | $"Route value parameter '{name}' must be string, string?, int, int?, long, or long?.")); |
| | 1 | 358 | | continue; |
| | | 359 | | } |
| | | 360 | | |
| | 12 | 361 | | foreach (IReadOnlyDictionary<string, RouteValueDefinition> values in routeValues) |
| | | 362 | | { |
| | 3 | 363 | | RouteValueDefinition routeValue = values[name]; |
| | | 364 | | |
| | 3 | 365 | | if (routeValue.IsOptional != IsNullableRouteValueParameter(parameter.Type)) |
| | | 366 | | { |
| | 3 | 367 | | string expectedNullability = routeValue.IsOptional ? "nullable" : "non-nullable"; |
| | 3 | 368 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 3 | 369 | | InvalidRouteValue, |
| | 3 | 370 | | parameter.Locations.FirstOrDefault(static candidate => candidate.IsInSource) ?? location, |
| | 3 | 371 | | $"Route value parameter '{name}' must be {expectedNullability} for the selected route template." |
| | 3 | 372 | | continue; |
| | | 373 | | } |
| | | 374 | | |
| | 0 | 375 | | if (routeValue.Constraint is { } constraint && |
| | 0 | 376 | | !RouteConstraintMatchesParameter(constraint, parameter.Type)) |
| | | 377 | | { |
| | 0 | 378 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 0 | 379 | | InvalidRouteValue, |
| | 0 | 380 | | parameter.Locations.FirstOrDefault(static candidate => candidate.IsInSource) ?? location, |
| | 0 | 381 | | $"Route value parameter '{name}' does not match route constraint '{constraint}'.")); |
| | | 382 | | } |
| | | 383 | | } |
| | | 384 | | } |
| | 6 | 385 | | } |
| | | 386 | | |
| | | 387 | | private static void AnalyzeBuiltInFilters( |
| | | 388 | | SymbolAnalysisContext context, |
| | | 389 | | IMethodSymbol method, |
| | | 390 | | HandlerKind routeKind, |
| | | 391 | | Location? location) |
| | | 392 | | { |
| | 86 | 393 | | TelegramHandlerMetadataRouteKind metadataRouteKind = ToMetadataRouteKind(routeKind); |
| | 86 | 394 | | AttributeData[] filterAttributes = TelegramBuiltInFilterFacts.GetAttributes(method.ContainingType) |
| | 86 | 395 | | .Concat(TelegramBuiltInFilterFacts.GetAttributes(method)) |
| | 86 | 396 | | .ToArray(); |
| | | 397 | | |
| | 233 | 398 | | foreach (AttributeData attribute in filterAttributes) |
| | | 399 | | { |
| | 34 | 400 | | if (!TelegramBuiltInFilterFacts.TryGetSpec(attribute, out TelegramBuiltInFilterSpec spec)) |
| | | 401 | | { |
| | | 402 | | continue; |
| | | 403 | | } |
| | | 404 | | |
| | 34 | 405 | | if (!TelegramBuiltInFilterFacts.SupportsRouteKind(spec.Target, metadataRouteKind)) |
| | | 406 | | { |
| | 7 | 407 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 7 | 408 | | InvalidFilter, |
| | 7 | 409 | | location, |
| | 7 | 410 | | TelegramBuiltInFilterFacts.GetInvalidFilterMessage(spec.Target, metadataRouteKind))); |
| | 7 | 411 | | return; |
| | | 412 | | } |
| | | 413 | | } |
| | | 414 | | |
| | 79 | 415 | | AnalyzeChatTypeFilters( |
| | 79 | 416 | | context, |
| | 79 | 417 | | method, |
| | 79 | 418 | | TelegramHandlerSymbols.ChatTypeAttribute, |
| | 79 | 419 | | "ChatTypeAttribute", |
| | 79 | 420 | | location); |
| | 79 | 421 | | AnalyzeChatTypeFilters( |
| | 79 | 422 | | context, |
| | 79 | 423 | | method, |
| | 79 | 424 | | TelegramHandlerSymbols.SenderChatTypeAttribute, |
| | 79 | 425 | | "SenderChatTypeAttribute", |
| | 79 | 426 | | location); |
| | | 427 | | |
| | 164 | 428 | | foreach (AttributeData attribute in TelegramHandlerSymbols.GetAttributes(method.ContainingType, TelegramHandlerS |
| | 79 | 429 | | .Concat(TelegramHandlerSymbols.GetAttributes(method, TelegramHandlerSymbols.ChatIdAttribute, inheri |
| | | 430 | | { |
| | 3 | 431 | | if (attribute.ConstructorArguments.Length == 0 || |
| | 3 | 432 | | attribute.ConstructorArguments[0].Values.IsDefaultOrEmpty || |
| | 6 | 433 | | attribute.ConstructorArguments[0].Values.Any(static value => value.Value is not long chatId || chatId == |
| | | 434 | | { |
| | 1 | 435 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 436 | | InvalidFilter, |
| | 1 | 437 | | location, |
| | 1 | 438 | | "ChatIdAttribute must specify at least one non-zero Telegram chat id.")); |
| | | 439 | | } |
| | | 440 | | } |
| | | 441 | | |
| | 164 | 442 | | foreach (AttributeData attribute in TelegramHandlerSymbols.GetAttributes(method.ContainingType, TelegramHandlerS |
| | 79 | 443 | | .Concat(TelegramHandlerSymbols.GetAttributes(method, TelegramHandlerSymbols.ChatUsernameAttribute, |
| | | 444 | | { |
| | 3 | 445 | | if (attribute.ConstructorArguments.Length == 0 || |
| | 3 | 446 | | attribute.ConstructorArguments[0].Values.IsDefaultOrEmpty || |
| | 6 | 447 | | attribute.ConstructorArguments[0].Values.Any(static value => value.Value is not string username || Canon |
| | | 448 | | { |
| | 1 | 449 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 450 | | InvalidFilter, |
| | 1 | 451 | | location, |
| | 1 | 452 | | "ChatUsernameAttribute must specify at least one non-empty Telegram chat username.")); |
| | | 453 | | } |
| | | 454 | | } |
| | | 455 | | |
| | 79 | 456 | | AnalyzeSenderIdFilters( |
| | 79 | 457 | | context, |
| | 79 | 458 | | method, |
| | 79 | 459 | | TelegramHandlerSymbols.FromUserAttribute, |
| | 79 | 460 | | "FromUserAttribute", |
| | 79 | 461 | | "user", |
| | 79 | 462 | | location); |
| | 79 | 463 | | AnalyzeSenderIdFilters( |
| | 79 | 464 | | context, |
| | 79 | 465 | | method, |
| | 79 | 466 | | TelegramHandlerSymbols.FromBotAttribute, |
| | 79 | 467 | | "FromBotAttribute", |
| | 79 | 468 | | "bot", |
| | 79 | 469 | | location); |
| | | 470 | | |
| | 162 | 471 | | foreach (AttributeData attribute in TelegramHandlerSymbols.GetAttributes(method.ContainingType, TelegramHandlerS |
| | 79 | 472 | | .Concat(TelegramHandlerSymbols.GetAttributes(method, TelegramHandlerSymbols.CallbackDataPrefixAttri |
| | | 473 | | { |
| | 2 | 474 | | if (attribute.ConstructorArguments.Length == 0 || |
| | 2 | 475 | | attribute.ConstructorArguments[0].Value is not string prefix || |
| | 2 | 476 | | string.IsNullOrWhiteSpace(prefix)) |
| | | 477 | | { |
| | 1 | 478 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 479 | | InvalidFilter, |
| | 1 | 480 | | location, |
| | 1 | 481 | | "CallbackDataPrefixAttribute must specify a non-empty callback data prefix.")); |
| | | 482 | | } |
| | | 483 | | } |
| | | 484 | | |
| | 162 | 485 | | foreach (AttributeData attribute in TelegramHandlerSymbols.GetAttributes(method.ContainingType, TelegramHandlerS |
| | 79 | 486 | | .Concat(TelegramHandlerSymbols.GetAttributes(method, TelegramHandlerSymbols.MessageThreadIdAttribut |
| | | 487 | | { |
| | 2 | 488 | | if (attribute.ConstructorArguments.Length == 0 || |
| | 2 | 489 | | attribute.ConstructorArguments[0].Values.IsDefaultOrEmpty || |
| | 4 | 490 | | attribute.ConstructorArguments[0].Values.Any(static value => value.Value is not long threadId || threadI |
| | | 491 | | { |
| | 1 | 492 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 493 | | InvalidFilter, |
| | 1 | 494 | | location, |
| | 1 | 495 | | "MessageThreadIdAttribute must specify at least one positive Telegram message thread id.")); |
| | | 496 | | } |
| | | 497 | | } |
| | 79 | 498 | | } |
| | | 499 | | |
| | | 500 | | private static void AnalyzeChatTypeFilters( |
| | | 501 | | SymbolAnalysisContext context, |
| | | 502 | | IMethodSymbol method, |
| | | 503 | | string attributeMetadataName, |
| | | 504 | | string attributeDisplayName, |
| | | 505 | | Location? location) |
| | | 506 | | { |
| | 330 | 507 | | foreach (AttributeData attribute in TelegramHandlerSymbols.GetAttributes( |
| | 158 | 508 | | method.ContainingType, |
| | 158 | 509 | | attributeMetadataName, |
| | 158 | 510 | | inherit: true) |
| | 158 | 511 | | .Concat(TelegramHandlerSymbols.GetAttributes( |
| | 158 | 512 | | method, |
| | 158 | 513 | | attributeMetadataName, |
| | 158 | 514 | | inherit: true))) |
| | | 515 | | { |
| | 7 | 516 | | if (attribute.ConstructorArguments.Length == 0 || |
| | 7 | 517 | | attribute.ConstructorArguments[0].Values.IsDefaultOrEmpty || |
| | 7 | 518 | | attribute.ConstructorArguments[0].Values.Any( |
| | 12 | 519 | | static value => value.Value is not int chatType || |
| | 12 | 520 | | !TelegramChatTypeFacts.IsKnown(chatType))) |
| | | 521 | | { |
| | 6 | 522 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 6 | 523 | | InvalidFilter, |
| | 6 | 524 | | location, |
| | 6 | 525 | | $"{attributeDisplayName} must specify at least one known Telegram chat type.")); |
| | | 526 | | } |
| | | 527 | | } |
| | 158 | 528 | | } |
| | | 529 | | |
| | | 530 | | private static void AnalyzeSenderIdFilters( |
| | | 531 | | SymbolAnalysisContext context, |
| | | 532 | | IMethodSymbol method, |
| | | 533 | | string attributeMetadataName, |
| | | 534 | | string attributeDisplayName, |
| | | 535 | | string senderDisplayName, |
| | | 536 | | Location? location) |
| | | 537 | | { |
| | 328 | 538 | | foreach (AttributeData attribute in TelegramHandlerSymbols.GetAttributes( |
| | 158 | 539 | | method.ContainingType, |
| | 158 | 540 | | attributeMetadataName, |
| | 158 | 541 | | inherit: true) |
| | 158 | 542 | | .Concat(TelegramHandlerSymbols.GetAttributes( |
| | 158 | 543 | | method, |
| | 158 | 544 | | attributeMetadataName, |
| | 158 | 545 | | inherit: true))) |
| | | 546 | | { |
| | 6 | 547 | | if (attribute.ConstructorArguments.Length != 1 || |
| | 6 | 548 | | attribute.ConstructorArguments[0].Values.IsDefault || |
| | 6 | 549 | | attribute.ConstructorArguments[0].Values.Any( |
| | 10 | 550 | | static value => value.Value is not long id || id <= 0)) |
| | | 551 | | { |
| | 2 | 552 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 2 | 553 | | InvalidFilter, |
| | 2 | 554 | | location, |
| | 2 | 555 | | $"{attributeDisplayName} must contain only positive Telegram {senderDisplayName} ids.")); |
| | | 556 | | } |
| | | 557 | | } |
| | 158 | 558 | | } |
| | | 559 | | |
| | | 560 | | private static void AnalyzeCustomFilters( |
| | | 561 | | SymbolAnalysisContext context, |
| | | 562 | | IMethodSymbol method, |
| | | 563 | | HandlerKind routeKind, |
| | | 564 | | Location? location) |
| | | 565 | | { |
| | 184 | 566 | | foreach (AttributeData attribute in GetCustomFilterAttributes(method.ContainingType) |
| | 86 | 567 | | .Concat(GetCustomFilterAttributes(method))) |
| | | 568 | | { |
| | 6 | 569 | | if (attribute.AttributeClass is not INamedTypeSymbol { TypeArguments.Length: 1 } attributeType) |
| | | 570 | | { |
| | 0 | 571 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 0 | 572 | | InvalidFilter, |
| | 0 | 573 | | location, |
| | 0 | 574 | | "UseFilterAttribute<TFilter> must specify a filter type.")); |
| | 0 | 575 | | continue; |
| | | 576 | | } |
| | | 577 | | |
| | 6 | 578 | | ITypeSymbol filterType = attributeType.TypeArguments[0]; |
| | | 579 | | |
| | 6 | 580 | | if (IsInvalidCustomFilterType(filterType)) |
| | | 581 | | { |
| | 1 | 582 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 583 | | InvalidFilter, |
| | 1 | 584 | | location, |
| | 1 | 585 | | "Custom Telegram filter type must be a concrete closed type.")); |
| | 1 | 586 | | continue; |
| | | 587 | | } |
| | | 588 | | |
| | 5 | 589 | | if (!TryGetTelegramFilterContextTypes(filterType, out IReadOnlyList<string> contextTypes)) |
| | | 590 | | { |
| | 1 | 591 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 592 | | InvalidFilter, |
| | 1 | 593 | | location, |
| | 1 | 594 | | "Custom Telegram filter type must implement ITelegramFilter<TContext>.")); |
| | 1 | 595 | | continue; |
| | | 596 | | } |
| | | 597 | | |
| | 4 | 598 | | if (!CustomFilterSupportsRouteKind(contextTypes, routeKind)) |
| | | 599 | | { |
| | 2 | 600 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 2 | 601 | | InvalidFilter, |
| | 2 | 602 | | location, |
| | 2 | 603 | | "Custom Telegram filter context type is not compatible with the handler route kind.")); |
| | | 604 | | } |
| | | 605 | | } |
| | | 606 | | |
| | 174 | 607 | | foreach (AttributeData attribute in TelegramHandlerSymbols.GetTelegramFilterAttributes(method.ContainingType, in |
| | 86 | 608 | | .Concat(TelegramHandlerSymbols.GetTelegramFilterAttributes(method, inherit: true))) |
| | | 609 | | { |
| | 1 | 610 | | if (attribute.AttributeClass is not { } attributeType || |
| | 1 | 611 | | !TelegramHandlerSymbols.TryGetTelegramFilterAttributeFilterType(attribute, out ITypeSymbol filterType)) |
| | | 612 | | { |
| | | 613 | | continue; |
| | | 614 | | } |
| | | 615 | | |
| | 1 | 616 | | if (attributeType.IsGenericType) |
| | | 617 | | { |
| | 0 | 618 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 0 | 619 | | InvalidFilter, |
| | 0 | 620 | | location, |
| | 0 | 621 | | "Parameterized custom Telegram filter attribute types must be non-generic.")); |
| | 0 | 622 | | continue; |
| | | 623 | | } |
| | | 624 | | |
| | 1 | 625 | | if (IsInvalidCustomFilterType(filterType)) |
| | | 626 | | { |
| | 0 | 627 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 0 | 628 | | InvalidFilter, |
| | 0 | 629 | | location, |
| | 0 | 630 | | "Custom Telegram filter type must be a concrete closed type.")); |
| | 0 | 631 | | continue; |
| | | 632 | | } |
| | | 633 | | |
| | 1 | 634 | | if (!TryGetParameterizedTelegramFilterContextTypes(filterType, attributeType, out IReadOnlyList<string> type |
| | | 635 | | { |
| | 1 | 636 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 637 | | InvalidFilter, |
| | 1 | 638 | | location, |
| | 1 | 639 | | "Parameterized custom Telegram filter attributes require a filter type that implements ITelegramFilt |
| | 1 | 640 | | continue; |
| | | 641 | | } |
| | | 642 | | |
| | 0 | 643 | | if (!CustomFilterSupportsRouteKind(typedContextTypes, routeKind)) |
| | | 644 | | { |
| | 0 | 645 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 0 | 646 | | InvalidFilter, |
| | 0 | 647 | | location, |
| | 0 | 648 | | "Custom Telegram filter context type is not compatible with the handler route kind.")); |
| | | 649 | | } |
| | | 650 | | } |
| | 86 | 651 | | } |
| | | 652 | | |
| | | 653 | | private static TelegramHandlerMetadataRouteKind ToMetadataRouteKind(HandlerKind routeKind) |
| | | 654 | | { |
| | 86 | 655 | | return routeKind switch |
| | 86 | 656 | | { |
| | 0 | 657 | | HandlerKind.Command => TelegramHandlerMetadataRouteKind.Command, |
| | 62 | 658 | | HandlerKind.Message => TelegramHandlerMetadataRouteKind.Message, |
| | 14 | 659 | | HandlerKind.Callback => TelegramHandlerMetadataRouteKind.Callback, |
| | 10 | 660 | | HandlerKind.ChatMember => TelegramHandlerMetadataRouteKind.ChatMember, |
| | 0 | 661 | | _ => TelegramHandlerMetadataRouteKind.Message |
| | 86 | 662 | | }; |
| | | 663 | | } |
| | | 664 | | |
| | | 665 | | private static string CanonicalizeChatUsername(string? username) |
| | | 666 | | { |
| | 3 | 667 | | if (username is null) |
| | | 668 | | { |
| | 0 | 669 | | return string.Empty; |
| | | 670 | | } |
| | | 671 | | |
| | 3 | 672 | | string value = username.Trim(); |
| | 3 | 673 | | return value.StartsWith("@", StringComparison.Ordinal) ? value.Substring(1) : value; |
| | | 674 | | } |
| | | 675 | | |
| | | 676 | | private static IReadOnlyList<AttributeData> GetCustomFilterAttributes(ISymbol symbol) |
| | | 677 | | { |
| | 172 | 678 | | return TelegramHandlerSymbols.GetGenericAttributes( |
| | 172 | 679 | | symbol, |
| | 172 | 680 | | TelegramHandlerSymbols.GenericUseFilterAttribute, |
| | 172 | 681 | | inherit: true); |
| | | 682 | | } |
| | | 683 | | |
| | | 684 | | private static bool IsInvalidCustomFilterType(ITypeSymbol type) |
| | | 685 | | { |
| | 7 | 686 | | if (type.TypeKind is TypeKind.Interface or TypeKind.TypeParameter) |
| | | 687 | | { |
| | 0 | 688 | | return true; |
| | | 689 | | } |
| | | 690 | | |
| | 7 | 691 | | if (type is not INamedTypeSymbol namedType) |
| | | 692 | | { |
| | 0 | 693 | | return true; |
| | | 694 | | } |
| | | 695 | | |
| | 7 | 696 | | return namedType.IsAbstract || |
| | 7 | 697 | | namedType.IsUnboundGenericType || |
| | 7 | 698 | | namedType.TypeArguments.Any(static argument => argument.TypeKind == TypeKind.TypeParameter); |
| | | 699 | | } |
| | | 700 | | |
| | | 701 | | private static bool TryGetTelegramFilterContextTypes( |
| | | 702 | | ITypeSymbol filterType, |
| | | 703 | | out IReadOnlyList<string> contextTypes) |
| | | 704 | | { |
| | 5 | 705 | | string[] values = filterType.AllInterfaces |
| | 5 | 706 | | .Where(static candidate => |
| | 4 | 707 | | string.Equals( |
| | 4 | 708 | | candidate.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), |
| | 4 | 709 | | TelegramHandlerSymbols.GenericTelegramFilter, |
| | 4 | 710 | | StringComparison.Ordinal)) |
| | 4 | 711 | | .Select(static candidate => candidate.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.CSharpErrorMessag |
| | 5 | 712 | | .ToArray(); |
| | | 713 | | |
| | 5 | 714 | | contextTypes = values; |
| | 5 | 715 | | return values.Length > 0; |
| | | 716 | | } |
| | | 717 | | |
| | | 718 | | private static bool TryGetParameterizedTelegramFilterContextTypes( |
| | | 719 | | ITypeSymbol filterType, |
| | | 720 | | ITypeSymbol attributeType, |
| | | 721 | | out IReadOnlyList<string> contextTypes) |
| | | 722 | | { |
| | 1 | 723 | | string[] values = filterType.AllInterfaces |
| | 1 | 724 | | .Where(candidate => |
| | 1 | 725 | | string.Equals( |
| | 1 | 726 | | candidate.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), |
| | 1 | 727 | | TelegramHandlerSymbols.GenericParameterizedTelegramFilter, |
| | 1 | 728 | | StringComparison.Ordinal) && |
| | 1 | 729 | | candidate.TypeArguments.Length == 2 && |
| | 1 | 730 | | SymbolEqualityComparer.Default.Equals(candidate.TypeArguments[1], attributeType)) |
| | 0 | 731 | | .Select(static candidate => candidate.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.CSharpErrorMessag |
| | 1 | 732 | | .ToArray(); |
| | | 733 | | |
| | 1 | 734 | | contextTypes = values; |
| | 1 | 735 | | return values.Length > 0; |
| | | 736 | | } |
| | | 737 | | |
| | | 738 | | private static bool CustomFilterSupportsRouteKind( |
| | | 739 | | IReadOnlyList<string> contextTypes, |
| | | 740 | | HandlerKind routeKind) |
| | | 741 | | { |
| | 4 | 742 | | if (contextTypes.Contains(TelegramHandlerSymbols.TelegramUpdateContext, StringComparer.Ordinal)) |
| | | 743 | | { |
| | 1 | 744 | | return true; |
| | | 745 | | } |
| | | 746 | | |
| | 3 | 747 | | string expectedContext = routeKind switch |
| | 3 | 748 | | { |
| | 1 | 749 | | HandlerKind.Callback => TelegramHandlerSymbols.CallbackQueryContext, |
| | 1 | 750 | | HandlerKind.ChatMember => TelegramHandlerSymbols.ChatMemberUpdatedContext, |
| | 1 | 751 | | _ => TelegramHandlerSymbols.MessageContext |
| | 3 | 752 | | }; |
| | | 753 | | |
| | 3 | 754 | | return contextTypes.Contains(expectedContext, StringComparer.Ordinal); |
| | | 755 | | } |
| | | 756 | | |
| | | 757 | | private static void AnalyzeChatMemberTransitions( |
| | | 758 | | SymbolAnalysisContext context, |
| | | 759 | | IMethodSymbol method, |
| | | 760 | | HandlerKind routeKind, |
| | | 761 | | Location? location) |
| | | 762 | | { |
| | 86 | 763 | | AttributeData[] transitionAttributes = TelegramHandlerSymbols.GetAttributes(method.ContainingType, TelegramHandl |
| | 86 | 764 | | .Concat(TelegramHandlerSymbols.GetAttributes(method, TelegramHandlerSymbols.ChatMemberTransitionAttribute, i |
| | 86 | 765 | | .ToArray(); |
| | 86 | 766 | | AttributeData[] changedAttributes = TelegramHandlerSymbols.GetAttributes(method.ContainingType, TelegramHandlerS |
| | 86 | 767 | | .Concat(TelegramHandlerSymbols.GetAttributes(method, TelegramHandlerSymbols.ChatMemberChangedAttribute, inhe |
| | 86 | 768 | | .ToArray(); |
| | | 769 | | |
| | 86 | 770 | | if (transitionAttributes.Length == 0 && changedAttributes.Length == 0) |
| | | 771 | | { |
| | 83 | 772 | | return; |
| | | 773 | | } |
| | | 774 | | |
| | 3 | 775 | | if (routeKind != HandlerKind.ChatMember) |
| | | 776 | | { |
| | 1 | 777 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 778 | | InvalidFilter, |
| | 1 | 779 | | location, |
| | 1 | 780 | | "Chat member transition attributes can only be used on chat member update handlers.")); |
| | 1 | 781 | | return; |
| | | 782 | | } |
| | | 783 | | |
| | 6 | 784 | | foreach (AttributeData attribute in transitionAttributes) |
| | | 785 | | { |
| | 1 | 786 | | if (attribute.ConstructorArguments.Length == 0 || |
| | 1 | 787 | | attribute.ConstructorArguments[0].Value is not int transition || |
| | 1 | 788 | | !TelegramMemberStatusFacts.TryMapTransition(transition, out _, out _)) |
| | | 789 | | { |
| | 1 | 790 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 791 | | InvalidFilter, |
| | 1 | 792 | | location, |
| | 1 | 793 | | "ChatMemberTransitionAttribute must specify a known Telegram member transition.")); |
| | | 794 | | } |
| | | 795 | | } |
| | | 796 | | |
| | 6 | 797 | | foreach (AttributeData attribute in changedAttributes) |
| | | 798 | | { |
| | 1 | 799 | | if (attribute.ConstructorArguments.Length < 2 || |
| | 1 | 800 | | attribute.ConstructorArguments[0].Value is not int oldStatus || |
| | 1 | 801 | | attribute.ConstructorArguments[1].Value is not int newStatus || |
| | 1 | 802 | | !TelegramMemberStatusFacts.IsValid(oldStatus) || |
| | 1 | 803 | | !TelegramMemberStatusFacts.IsValid(newStatus)) |
| | | 804 | | { |
| | 1 | 805 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 806 | | InvalidFilter, |
| | 1 | 807 | | location, |
| | 1 | 808 | | "ChatMemberChangedAttribute must specify non-empty known Telegram member status sets.")); |
| | | 809 | | } |
| | | 810 | | } |
| | 2 | 811 | | } |
| | | 812 | | |
| | | 813 | | private static void AnalyzeTelegramRoleRequirements( |
| | | 814 | | SymbolAnalysisContext context, |
| | | 815 | | IMethodSymbol method, |
| | | 816 | | Location? location) |
| | | 817 | | { |
| | 174 | 818 | | foreach (AttributeData attribute in TelegramHandlerSymbols.GetAttributes(method.ContainingType, TelegramHandlerS |
| | 86 | 819 | | .Concat(TelegramHandlerSymbols.GetAttributes(method, TelegramHandlerSymbols.RequireTelegramRoleAttr |
| | | 820 | | { |
| | 1 | 821 | | if (!TelegramMemberStatusFacts.TryGetRoleRequirementMask(attribute, out int allowedStatuses) || |
| | 1 | 822 | | !TelegramMemberStatusFacts.IsValid(allowedStatuses)) |
| | | 823 | | { |
| | 1 | 824 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 825 | | InvalidFilter, |
| | 1 | 826 | | location, |
| | 1 | 827 | | "RequireTelegramRoleAttribute must specify non-empty known Telegram member status sets.")); |
| | | 828 | | } |
| | | 829 | | } |
| | 86 | 830 | | } |
| | | 831 | | |
| | | 832 | | private static bool TryParseRouteTemplate( |
| | | 833 | | string? template, |
| | | 834 | | out IReadOnlyDictionary<string, RouteValueDefinition> values, |
| | | 835 | | out string reason) |
| | | 836 | | { |
| | 12 | 837 | | values = new Dictionary<string, RouteValueDefinition>(StringComparer.Ordinal); |
| | 12 | 838 | | reason = string.Empty; |
| | | 839 | | |
| | 12 | 840 | | if (template is null || |
| | 12 | 841 | | string.IsNullOrWhiteSpace(template)) |
| | | 842 | | { |
| | 0 | 843 | | reason = "Route template must not be empty."; |
| | 0 | 844 | | return false; |
| | | 845 | | } |
| | | 846 | | |
| | 12 | 847 | | Regex placeholderRegex = new Regex( |
| | 12 | 848 | | @"\{(?<name>[A-Za-z_][A-Za-z0-9_]*)(?:(?<nameOptional>\?)|:(?<constraint>[A-Za-z][A-Za-z0-9_]*)(?<constraint |
| | 12 | 849 | | RegexOptions.CultureInvariant); |
| | 12 | 850 | | Dictionary<string, RouteValueDefinition> parsedValues = new Dictionary<string, RouteValueDefinition>(StringCompa |
| | 12 | 851 | | int position = 0; |
| | | 852 | | |
| | 45 | 853 | | foreach (Match match in placeholderRegex.Matches(template)) |
| | | 854 | | { |
| | 12 | 855 | | if (match.Index != position && |
| | 12 | 856 | | ContainsBrace(template.Substring(position, match.Index - position))) |
| | | 857 | | { |
| | 0 | 858 | | reason = $"Route template '{template}' contains an invalid placeholder."; |
| | 0 | 859 | | return false; |
| | | 860 | | } |
| | | 861 | | |
| | 12 | 862 | | string name = match.Groups["name"].Value; |
| | 12 | 863 | | bool hasNameOptional = match.Groups["nameOptional"].Success; |
| | 12 | 864 | | bool hasConstraintOptional = match.Groups["constraintOptional"].Success; |
| | 12 | 865 | | string constraint = match.Groups["constraint"].Success |
| | 12 | 866 | | ? match.Groups["constraint"].Value |
| | 12 | 867 | | : "string"; |
| | | 868 | | |
| | 12 | 869 | | if (parsedValues.ContainsKey(name)) |
| | | 870 | | { |
| | 1 | 871 | | reason = $"Route template '{template}' contains duplicate placeholder '{name}'."; |
| | 1 | 872 | | return false; |
| | | 873 | | } |
| | | 874 | | |
| | 11 | 875 | | parsedValues.Add(name, new RouteValueDefinition(constraint, hasNameOptional || hasConstraintOptional)); |
| | | 876 | | |
| | 11 | 877 | | if (constraint is not ("string" or "int" or "long")) |
| | | 878 | | { |
| | 2 | 879 | | reason = $"Route template '{template}' uses unsupported placeholder constraint '{constraint}'."; |
| | 2 | 880 | | return false; |
| | | 881 | | } |
| | | 882 | | |
| | 9 | 883 | | position = match.Index + match.Length; |
| | | 884 | | } |
| | | 885 | | |
| | 9 | 886 | | if (ContainsBrace(template.Substring(position))) |
| | | 887 | | { |
| | 1 | 888 | | reason = $"Route template '{template}' contains an invalid placeholder."; |
| | 1 | 889 | | return false; |
| | | 890 | | } |
| | | 891 | | |
| | 8 | 892 | | values = parsedValues; |
| | 8 | 893 | | return true; |
| | 3 | 894 | | } |
| | | 895 | | |
| | | 896 | | private static bool TryParseRouteRegex( |
| | | 897 | | string? pattern, |
| | | 898 | | out IReadOnlyDictionary<string, RouteValueDefinition> values, |
| | | 899 | | out string reason) |
| | | 900 | | { |
| | 3 | 901 | | values = new Dictionary<string, RouteValueDefinition>(StringComparer.Ordinal); |
| | 3 | 902 | | reason = string.Empty; |
| | | 903 | | |
| | 3 | 904 | | if (string.IsNullOrWhiteSpace(pattern)) |
| | | 905 | | { |
| | 0 | 906 | | reason = "Route regex pattern must not be empty."; |
| | 0 | 907 | | return false; |
| | | 908 | | } |
| | | 909 | | |
| | | 910 | | Regex regex; |
| | | 911 | | |
| | | 912 | | try |
| | | 913 | | { |
| | 3 | 914 | | regex = new Regex(pattern, RegexOptions.CultureInvariant); |
| | 2 | 915 | | } |
| | 1 | 916 | | catch (ArgumentException exception) |
| | | 917 | | { |
| | 1 | 918 | | reason = $"Route regex pattern is invalid: {exception.Message}"; |
| | 1 | 919 | | return false; |
| | | 920 | | } |
| | | 921 | | |
| | 2 | 922 | | values = regex |
| | 2 | 923 | | .GetGroupNames() |
| | 4 | 924 | | .Where(static name => !int.TryParse(name, out _)) |
| | 2 | 925 | | .ToDictionary( |
| | 2 | 926 | | static name => name, |
| | 2 | 927 | | static _ => new RouteValueDefinition(Constraint: null, IsOptional: false), |
| | 2 | 928 | | StringComparer.Ordinal); |
| | 2 | 929 | | return true; |
| | 1 | 930 | | } |
| | | 931 | | |
| | | 932 | | private static string? GetConstructorString(AttributeData attribute) |
| | | 933 | | { |
| | 21 | 934 | | return attribute.ConstructorArguments.Length > 0 && |
| | 21 | 935 | | attribute.ConstructorArguments[0].Value is string value |
| | 21 | 936 | | ? value.Trim() |
| | 21 | 937 | | : null; |
| | | 938 | | } |
| | | 939 | | |
| | | 940 | | private static bool TryGetPrefixes( |
| | | 941 | | AttributeData attribute, |
| | | 942 | | out IReadOnlyList<string> prefixes, |
| | | 943 | | out string reason) |
| | | 944 | | { |
| | 9 | 945 | | prefixes = ["/"]; |
| | 9 | 946 | | reason = string.Empty; |
| | | 947 | | |
| | 24 | 948 | | foreach (KeyValuePair<string, TypedConstant> argument in attribute.NamedArguments) |
| | | 949 | | { |
| | 5 | 950 | | if (!string.Equals(argument.Key, "Prefixes", StringComparison.Ordinal)) |
| | | 951 | | { |
| | | 952 | | continue; |
| | | 953 | | } |
| | | 954 | | |
| | 4 | 955 | | if (argument.Value.Values.IsDefaultOrEmpty) |
| | | 956 | | { |
| | 0 | 957 | | reason = "Command route prefixes must contain at least one non-empty prefix."; |
| | 0 | 958 | | return false; |
| | | 959 | | } |
| | | 960 | | |
| | 4 | 961 | | List<string> values = new List<string>(); |
| | | 962 | | |
| | 14 | 963 | | foreach (TypedConstant value in argument.Value.Values) |
| | | 964 | | { |
| | 4 | 965 | | if (value.Value is not string prefix || |
| | 4 | 966 | | string.IsNullOrWhiteSpace(prefix)) |
| | | 967 | | { |
| | 2 | 968 | | reason = "Command route prefixes must contain only non-empty prefix values."; |
| | 2 | 969 | | return false; |
| | | 970 | | } |
| | | 971 | | |
| | 2 | 972 | | values.Add(prefix.Trim()); |
| | | 973 | | } |
| | | 974 | | |
| | 2 | 975 | | prefixes = values.Distinct(StringComparer.Ordinal).ToArray(); |
| | 2 | 976 | | return true; |
| | | 977 | | } |
| | | 978 | | |
| | 5 | 979 | | return true; |
| | | 980 | | } |
| | | 981 | | |
| | | 982 | | private static int GetCommandPrefixMode(AttributeData attribute) |
| | | 983 | | { |
| | 17 | 984 | | foreach (KeyValuePair<string, TypedConstant> argument in attribute.NamedArguments) |
| | | 985 | | { |
| | 2 | 986 | | if (string.Equals(argument.Key, "PrefixMode", StringComparison.Ordinal) && |
| | 2 | 987 | | argument.Value.Value is int value) |
| | | 988 | | { |
| | 1 | 989 | | return value; |
| | | 990 | | } |
| | | 991 | | } |
| | | 992 | | |
| | 6 | 993 | | return CommandPrefixModeRequired; |
| | | 994 | | } |
| | | 995 | | |
| | | 996 | | private static bool TryValidateCommandPrefixMode( |
| | | 997 | | AttributeData attribute, |
| | | 998 | | int prefixMode, |
| | | 999 | | out string reason) |
| | | 1000 | | { |
| | 7 | 1001 | | reason = string.Empty; |
| | | 1002 | | |
| | 7 | 1003 | | if (!IsSupportedCommandPrefixMode(prefixMode)) |
| | | 1004 | | { |
| | 0 | 1005 | | reason = "Command route prefix mode must be Required, Optional, or NoPrefix."; |
| | 0 | 1006 | | return false; |
| | | 1007 | | } |
| | | 1008 | | |
| | 7 | 1009 | | if (prefixMode == CommandPrefixModeNoPrefix && HasNamedArgument(attribute, "Prefixes")) |
| | | 1010 | | { |
| | 1 | 1011 | | reason = "Command route prefixes must not be configured when PrefixMode is NoPrefix."; |
| | 1 | 1012 | | return false; |
| | | 1013 | | } |
| | | 1014 | | |
| | 6 | 1015 | | return true; |
| | | 1016 | | } |
| | | 1017 | | |
| | | 1018 | | private static bool IsSupportedCommandPrefixMode(int value) |
| | | 1019 | | { |
| | 7 | 1020 | | return value is CommandPrefixModeRequired or |
| | 7 | 1021 | | CommandPrefixModeOptional or |
| | 7 | 1022 | | CommandPrefixModeNoPrefix; |
| | | 1023 | | } |
| | | 1024 | | |
| | | 1025 | | private static bool HasNamedArgument( |
| | | 1026 | | AttributeData attribute, |
| | | 1027 | | string name) |
| | | 1028 | | { |
| | 3 | 1029 | | return attribute.NamedArguments.Any(argument => string.Equals(argument.Key, name, StringComparison.Ordinal)); |
| | | 1030 | | } |
| | | 1031 | | |
| | | 1032 | | private static bool CommandPatternStartsWithPrefix( |
| | | 1033 | | string pattern, |
| | | 1034 | | IReadOnlyList<string> prefixes) |
| | | 1035 | | { |
| | 2 | 1036 | | string normalized = pattern.Trim(); |
| | | 1037 | | |
| | 4 | 1038 | | return prefixes.Any(prefix => normalized.StartsWith(prefix, StringComparison.Ordinal)); |
| | | 1039 | | } |
| | | 1040 | | |
| | | 1041 | | private static bool GetNamedBool( |
| | | 1042 | | AttributeData attribute, |
| | | 1043 | | string name, |
| | | 1044 | | bool defaultValue) |
| | | 1045 | | { |
| | 8 | 1046 | | foreach (KeyValuePair<string, TypedConstant> argument in attribute.NamedArguments) |
| | | 1047 | | { |
| | 0 | 1048 | | if (string.Equals(argument.Key, name, StringComparison.Ordinal) && |
| | 0 | 1049 | | argument.Value.Value is bool value) |
| | | 1050 | | { |
| | 0 | 1051 | | return value; |
| | | 1052 | | } |
| | | 1053 | | } |
| | | 1054 | | |
| | 4 | 1055 | | return defaultValue; |
| | | 1056 | | } |
| | | 1057 | | |
| | | 1058 | | private static bool ContainsBrace(string value) |
| | | 1059 | | { |
| | 21 | 1060 | | return value.IndexOf('{') >= 0 || |
| | 21 | 1061 | | value.IndexOf('}') >= 0; |
| | | 1062 | | } |
| | | 1063 | | |
| | | 1064 | | private static bool IsSupportedRouteValueParameterType(ITypeSymbol type) |
| | | 1065 | | { |
| | 4 | 1066 | | if (type.SpecialType is SpecialType.System_String or |
| | 4 | 1067 | | SpecialType.System_Int32 or |
| | 4 | 1068 | | SpecialType.System_Int64) |
| | | 1069 | | { |
| | 2 | 1070 | | return true; |
| | | 1071 | | } |
| | | 1072 | | |
| | 2 | 1073 | | return type is INamedTypeSymbol |
| | 2 | 1074 | | { |
| | 2 | 1075 | | OriginalDefinition.SpecialType: SpecialType.System_Nullable_T, |
| | 2 | 1076 | | TypeArguments.Length: 1 |
| | 2 | 1077 | | } nullable && |
| | 2 | 1078 | | nullable.TypeArguments[0].SpecialType is |
| | 2 | 1079 | | SpecialType.System_Int32 or |
| | 2 | 1080 | | SpecialType.System_Int64; |
| | | 1081 | | } |
| | | 1082 | | |
| | | 1083 | | private static bool IsNullableRouteValueParameter(ITypeSymbol type) |
| | | 1084 | | { |
| | 3 | 1085 | | if (type.SpecialType == SpecialType.System_String) |
| | | 1086 | | { |
| | 1 | 1087 | | return type.NullableAnnotation == NullableAnnotation.Annotated; |
| | | 1088 | | } |
| | | 1089 | | |
| | 2 | 1090 | | return type is INamedTypeSymbol |
| | 2 | 1091 | | { |
| | 2 | 1092 | | OriginalDefinition.SpecialType: SpecialType.System_Nullable_T |
| | 2 | 1093 | | }; |
| | | 1094 | | } |
| | | 1095 | | |
| | | 1096 | | private static SpecialType GetRouteValueParameterSpecialType(ITypeSymbol type) |
| | | 1097 | | { |
| | 0 | 1098 | | if (type is INamedTypeSymbol |
| | 0 | 1099 | | { |
| | 0 | 1100 | | OriginalDefinition.SpecialType: SpecialType.System_Nullable_T, |
| | 0 | 1101 | | TypeArguments.Length: 1 |
| | 0 | 1102 | | } nullable) |
| | | 1103 | | { |
| | 0 | 1104 | | return nullable.TypeArguments[0].SpecialType; |
| | | 1105 | | } |
| | | 1106 | | |
| | 0 | 1107 | | return type.SpecialType; |
| | | 1108 | | } |
| | | 1109 | | |
| | | 1110 | | private static bool RouteConstraintMatchesParameter( |
| | | 1111 | | string constraint, |
| | | 1112 | | ITypeSymbol parameterType) |
| | | 1113 | | { |
| | 0 | 1114 | | SpecialType specialType = GetRouteValueParameterSpecialType(parameterType); |
| | | 1115 | | |
| | 0 | 1116 | | return constraint switch |
| | 0 | 1117 | | { |
| | 0 | 1118 | | "string" => specialType == SpecialType.System_String, |
| | 0 | 1119 | | "int" => specialType == SpecialType.System_Int32, |
| | 0 | 1120 | | "long" => specialType == SpecialType.System_Int64, |
| | 0 | 1121 | | _ => false |
| | 0 | 1122 | | }; |
| | | 1123 | | } |
| | | 1124 | | |
| | | 1125 | | private readonly record struct RouteValueDefinition( |
| | | 1126 | | string? Constraint, |
| | | 1127 | | bool IsOptional); |
| | | 1128 | | |
| | | 1129 | | private static void ValidateCallbackRoute( |
| | | 1130 | | SymbolAnalysisContext context, |
| | | 1131 | | IMethodSymbol method, |
| | | 1132 | | ITypeSymbol? callbackPayloadType, |
| | | 1133 | | bool hasRawCallback, |
| | | 1134 | | Location? location) |
| | | 1135 | | { |
| | 14 | 1136 | | if (callbackPayloadType is null) |
| | | 1137 | | { |
| | 8 | 1138 | | if (hasRawCallback) |
| | | 1139 | | { |
| | 27 | 1140 | | foreach (IParameterSymbol parameter in method.Parameters.Where(static parameter => HasCallbackDataAttrib |
| | | 1141 | | { |
| | 1 | 1142 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 1143 | | InvalidTypedCallback, |
| | 1 | 1144 | | parameter.Locations.FirstOrDefault(static candidate => candidate.IsInSource) ?? location, |
| | 1 | 1145 | | $"Raw CallbackAttribute handler '{method.Name}' cannot bind typed callback payload parameter '{p |
| | | 1146 | | } |
| | | 1147 | | } |
| | | 1148 | | |
| | 8 | 1149 | | return; |
| | | 1150 | | } |
| | | 1151 | | |
| | 6 | 1152 | | if (IsInvalidCallbackPayloadType(callbackPayloadType)) |
| | | 1153 | | { |
| | 1 | 1154 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 1155 | | InvalidTypedCallback, |
| | 1 | 1156 | | location, |
| | 1 | 1157 | | $"CallbackAttribute<TPayload> on '{method.Name}' must use a concrete closed payload type.")); |
| | | 1158 | | } |
| | | 1159 | | |
| | 6 | 1160 | | IParameterSymbol[] payloadParameters = method.Parameters |
| | 12 | 1161 | | .Where(parameter => SymbolEqualityComparer.Default.Equals(parameter.Type, callbackPayloadType)) |
| | 6 | 1162 | | .ToArray(); |
| | | 1163 | | |
| | 6 | 1164 | | if (payloadParameters.Length != 1) |
| | | 1165 | | { |
| | 2 | 1166 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 2 | 1167 | | InvalidTypedCallback, |
| | 2 | 1168 | | location, |
| | 2 | 1169 | | $"CallbackAttribute<{callbackPayloadType.Name}> handler '{method.Name}' must declare exactly one {callba |
| | | 1170 | | } |
| | 6 | 1171 | | } |
| | | 1172 | | |
| | | 1173 | | private static bool HasCallbackDataAttribute(ITypeSymbol type) |
| | | 1174 | | { |
| | 9 | 1175 | | return type is INamedTypeSymbol namedType && |
| | 9 | 1176 | | TelegramHandlerSymbols.HasAttribute(namedType, TelegramHandlerSymbols.CallbackDataAttribute); |
| | | 1177 | | } |
| | | 1178 | | |
| | | 1179 | | private static bool IsInvalidCallbackPayloadType(ITypeSymbol type) |
| | | 1180 | | { |
| | 6 | 1181 | | if (type.TypeKind is TypeKind.Interface or TypeKind.TypeParameter) |
| | | 1182 | | { |
| | 1 | 1183 | | return true; |
| | | 1184 | | } |
| | | 1185 | | |
| | 5 | 1186 | | if (type is not INamedTypeSymbol namedType) |
| | | 1187 | | { |
| | 0 | 1188 | | return true; |
| | | 1189 | | } |
| | | 1190 | | |
| | 5 | 1191 | | return namedType.IsAbstract || |
| | 5 | 1192 | | namedType.IsUnboundGenericType || |
| | 5 | 1193 | | namedType.TypeArguments.Any(static argument => argument.TypeKind == TypeKind.TypeParameter); |
| | | 1194 | | } |
| | | 1195 | | |
| | | 1196 | | private static void ValidateContextParameter( |
| | | 1197 | | SymbolAnalysisContext context, |
| | | 1198 | | IMethodSymbol method, |
| | | 1199 | | HandlerKind kind, |
| | | 1200 | | string displayName, |
| | | 1201 | | Location? location) |
| | | 1202 | | { |
| | 86 | 1203 | | string expectedContextType = kind switch |
| | 86 | 1204 | | { |
| | 14 | 1205 | | HandlerKind.Callback => TelegramHandlerSymbols.CallbackQueryContext, |
| | 10 | 1206 | | HandlerKind.ChatMember => TelegramHandlerSymbols.ChatMemberUpdatedContext, |
| | 62 | 1207 | | _ => TelegramHandlerSymbols.MessageContext |
| | 86 | 1208 | | }; |
| | 86 | 1209 | | string expectedContextName = kind switch |
| | 86 | 1210 | | { |
| | 14 | 1211 | | HandlerKind.Callback => "CallbackQueryContext", |
| | 10 | 1212 | | HandlerKind.ChatMember => "ChatMemberUpdatedContext", |
| | 62 | 1213 | | _ => "MessageContext" |
| | 86 | 1214 | | }; |
| | 86 | 1215 | | IParameterSymbol[] contextParameters = method.Parameters |
| | 86 | 1216 | | .Where(static parameter => |
| | 107 | 1217 | | TelegramHandlerSymbols.IsType(parameter.Type, TelegramHandlerSymbols.MessageContext) || |
| | 107 | 1218 | | TelegramHandlerSymbols.IsType(parameter.Type, TelegramHandlerSymbols.CallbackQueryContext) || |
| | 107 | 1219 | | TelegramHandlerSymbols.IsType(parameter.Type, TelegramHandlerSymbols.ChatMemberUpdatedContext) || |
| | 107 | 1220 | | TelegramHandlerSymbols.IsType(parameter.Type, TelegramHandlerSymbols.TelegramUpdateContext)) |
| | 86 | 1221 | | .ToArray(); |
| | | 1222 | | |
| | 86 | 1223 | | if (contextParameters.Length != 1 || |
| | 86 | 1224 | | !TelegramHandlerSymbols.IsType(contextParameters[0].Type, expectedContextType)) |
| | | 1225 | | { |
| | 2 | 1226 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 2 | 1227 | | InvalidContextParameter, |
| | 2 | 1228 | | location, |
| | 2 | 1229 | | GetDiagnosticHandlerKind(kind), |
| | 2 | 1230 | | displayName, |
| | 2 | 1231 | | expectedContextName)); |
| | | 1232 | | } |
| | 86 | 1233 | | } |
| | | 1234 | | |
| | | 1235 | | private static string GetDiagnosticHandlerKind(HandlerKind kind) |
| | | 1236 | | { |
| | 2 | 1237 | | return kind switch |
| | 2 | 1238 | | { |
| | 0 | 1239 | | HandlerKind.Callback => "callback", |
| | 1 | 1240 | | HandlerKind.ChatMember => "chatmember", |
| | 1 | 1241 | | _ => "message" |
| | 2 | 1242 | | }; |
| | | 1243 | | } |
| | | 1244 | | |
| | | 1245 | | private static void ValidateCancellationTokens( |
| | | 1246 | | SymbolAnalysisContext context, |
| | | 1247 | | IMethodSymbol method, |
| | | 1248 | | string displayName, |
| | | 1249 | | Location? location) |
| | | 1250 | | { |
| | 86 | 1251 | | if (method.Parameters.Count(static parameter => |
| | 193 | 1252 | | TelegramHandlerSymbols.IsType(parameter.Type, TelegramHandlerSymbols.CancellationToken)) <= 1) |
| | | 1253 | | { |
| | 85 | 1254 | | return; |
| | | 1255 | | } |
| | | 1256 | | |
| | 1 | 1257 | | context.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 1258 | | MultipleCancellationTokens, |
| | 1 | 1259 | | location, |
| | 1 | 1260 | | displayName)); |
| | 1 | 1261 | | } |
| | | 1262 | | |
| | | 1263 | | private static bool IsSupportedReturnType(ITypeSymbol returnType) |
| | | 1264 | | { |
| | 86 | 1265 | | return TelegramHandlerSymbols.IsType(returnType, TelegramHandlerSymbols.Task) || |
| | 86 | 1266 | | TelegramHandlerSymbols.IsType(returnType, TelegramHandlerSymbols.ValueTask); |
| | | 1267 | | } |
| | | 1268 | | |
| | | 1269 | | private static bool IsValidCommand(string? command) |
| | | 1270 | | { |
| | 6 | 1271 | | return command is not null && |
| | 6 | 1272 | | !string.IsNullOrWhiteSpace(command) && |
| | 6 | 1273 | | !command.StartsWith("/", StringComparison.Ordinal) && |
| | 6 | 1274 | | !command.Contains("@", StringComparison.Ordinal) && |
| | 6 | 1275 | | !command.Any(char.IsWhiteSpace); |
| | | 1276 | | } |
| | | 1277 | | |
| | | 1278 | | private static bool HasAnyRouteAttribute( |
| | | 1279 | | IMethodSymbol method, |
| | | 1280 | | bool includeClassRouteAttributes) |
| | | 1281 | | { |
| | 146 | 1282 | | return TelegramHandlerSymbols.HasAnyRouteAttribute(method) || |
| | 146 | 1283 | | (includeClassRouteAttributes && |
| | 146 | 1284 | | TelegramHandlerSymbols.HasAnyRouteAttribute(method.ContainingType)); |
| | | 1285 | | } |
| | | 1286 | | |
| | | 1287 | | private static IReadOnlyList<AttributeData> GetRouteAttributes( |
| | | 1288 | | IMethodSymbol method, |
| | | 1289 | | string metadataName, |
| | | 1290 | | bool includeClassRouteAttributes) |
| | | 1291 | | { |
| | 1458 | 1292 | | IReadOnlyList<AttributeData> methodAttributes = TelegramHandlerSymbols.GetAttributes(method, metadataName, inher |
| | | 1293 | | |
| | 1458 | 1294 | | if (!includeClassRouteAttributes) |
| | | 1295 | | { |
| | 1265 | 1296 | | return methodAttributes; |
| | | 1297 | | } |
| | | 1298 | | |
| | 193 | 1299 | | IReadOnlyList<AttributeData> typeAttributes = TelegramHandlerSymbols.GetAttributes(method.ContainingType, metada |
| | | 1300 | | |
| | 193 | 1301 | | return typeAttributes.Count == 0 |
| | 193 | 1302 | | ? methodAttributes |
| | 193 | 1303 | | : typeAttributes.Concat(methodAttributes).ToArray(); |
| | | 1304 | | } |
| | | 1305 | | |
| | | 1306 | | private static IReadOnlyList<AttributeData> GetGenericRouteAttributes( |
| | | 1307 | | IMethodSymbol method, |
| | | 1308 | | string metadataName, |
| | | 1309 | | bool includeClassRouteAttributes) |
| | | 1310 | | { |
| | 97 | 1311 | | IReadOnlyList<AttributeData> methodAttributes = TelegramHandlerSymbols.GetGenericAttributes(method, metadataName |
| | | 1312 | | |
| | 97 | 1313 | | if (!includeClassRouteAttributes) |
| | | 1314 | | { |
| | 82 | 1315 | | return methodAttributes; |
| | | 1316 | | } |
| | | 1317 | | |
| | 15 | 1318 | | IReadOnlyList<AttributeData> typeAttributes = TelegramHandlerSymbols.GetGenericAttributes(method.ContainingType, |
| | | 1319 | | |
| | 15 | 1320 | | return typeAttributes.Count == 0 |
| | 15 | 1321 | | ? methodAttributes |
| | 15 | 1322 | | : typeAttributes.Concat(methodAttributes).ToArray(); |
| | | 1323 | | } |
| | | 1324 | | } |