|
1 | 1 |
|
2 | 2 | using System; |
| 3 | +using System.Collections.Generic; |
3 | 4 | using System.Collections.Immutable; |
4 | 5 | using System.Diagnostics; |
5 | 6 | using System.Linq; |
6 | 7 | using System.Text; |
7 | 8 | using Microsoft.CodeAnalysis; |
| 9 | +using Microsoft.CodeAnalysis.CSharp; |
| 10 | +using Microsoft.CodeAnalysis.CSharp.Syntax; |
8 | 11 |
|
9 | 12 | namespace Serde |
10 | 13 | { |
@@ -185,5 +188,292 @@ void AddWordAndClear() |
185 | 188 | } |
186 | 189 | } |
187 | 190 | } |
| 191 | + |
| 192 | + /// <summary> |
| 193 | + /// Returns the initializer expression text for the field/property with all symbols fully qualified, |
| 194 | + /// or null if there is no initializer or the initializer cannot be safely preserved. |
| 195 | + /// </summary> |
| 196 | + public string? GetInitializer(Compilation compilation) |
| 197 | + { |
| 198 | + foreach (var syntaxRef in Symbol.DeclaringSyntaxReferences) |
| 199 | + { |
| 200 | + var syntax = syntaxRef.GetSyntax(); |
| 201 | + EqualsValueClauseSyntax? initializer = syntax switch |
| 202 | + { |
| 203 | + VariableDeclaratorSyntax v => v.Initializer, |
| 204 | + PropertyDeclarationSyntax p => p.Initializer, |
| 205 | + _ => null |
| 206 | + }; |
| 207 | + if (initializer is not null) |
| 208 | + { |
| 209 | + var semanticModel = compilation.GetSemanticModel(syntax.SyntaxTree); |
| 210 | + var qualifiedExpr = QualifyExpression(initializer.Value, semanticModel); |
| 211 | + // If the rewriter returned null, the expression can't be safely preserved |
| 212 | + if (qualifiedExpr is null) |
| 213 | + { |
| 214 | + return null; |
| 215 | + } |
| 216 | + return qualifiedExpr.ToFullString(); |
| 217 | + } |
| 218 | + } |
| 219 | + return null; |
| 220 | + } |
| 221 | + |
| 222 | + /// <summary> |
| 223 | + /// Rewrites an expression to fully qualify all type references. |
| 224 | + /// Returns null if the expression cannot be safely preserved. |
| 225 | + /// </summary> |
| 226 | + private static ExpressionSyntax? QualifyExpression(ExpressionSyntax expr, SemanticModel semanticModel) |
| 227 | + { |
| 228 | + var rewriter = new FullyQualifyingRewriter(semanticModel); |
| 229 | + var result = rewriter.Visit(expr); |
| 230 | + if (rewriter.Failed) |
| 231 | + { |
| 232 | + return null; |
| 233 | + } |
| 234 | + return (ExpressionSyntax?)result; |
| 235 | + } |
| 236 | + |
| 237 | + /// <summary> |
| 238 | + /// Format for fully qualified symbol names with global:: prefix. |
| 239 | + /// </summary> |
| 240 | + private static readonly SymbolDisplayFormat s_fullyQualifiedFormat = SymbolDisplayFormat.FullyQualifiedFormat |
| 241 | + .WithMemberOptions(SymbolDisplayMemberOptions.IncludeContainingType); |
| 242 | + |
| 243 | + /// <summary> |
| 244 | + /// A syntax rewriter that fully qualifies all type and member references. |
| 245 | + /// Sets Failed to true if the expression cannot be safely preserved. |
| 246 | + /// </summary> |
| 247 | + private sealed class FullyQualifyingRewriter : CSharpSyntaxRewriter |
| 248 | + { |
| 249 | + private readonly SemanticModel _semanticModel; |
| 250 | + |
| 251 | + public bool Failed { get; private set; } |
| 252 | + |
| 253 | + public FullyQualifyingRewriter(SemanticModel semanticModel) |
| 254 | + { |
| 255 | + _semanticModel = semanticModel; |
| 256 | + } |
| 257 | + |
| 258 | + public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) |
| 259 | + { |
| 260 | + var symbolInfo = _semanticModel.GetSymbolInfo(node); |
| 261 | + if (symbolInfo.Symbol is ITypeSymbol typeSymbol) |
| 262 | + { |
| 263 | + var fqn = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); |
| 264 | + return SyntaxFactory.ParseTypeName(fqn).WithTriviaFrom(node); |
| 265 | + } |
| 266 | + return base.VisitIdentifierName(node); |
| 267 | + } |
| 268 | + |
| 269 | + public override SyntaxNode? VisitPredefinedType(PredefinedTypeSyntax node) |
| 270 | + { |
| 271 | + // Predefined types like 'string', 'int' are always in scope - no need to qualify |
| 272 | + return base.VisitPredefinedType(node); |
| 273 | + } |
| 274 | + |
| 275 | + public override SyntaxNode? VisitMemberAccessExpression(MemberAccessExpressionSyntax node) |
| 276 | + { |
| 277 | + var symbolInfo = _semanticModel.GetSymbolInfo(node); |
| 278 | + var symbol = symbolInfo.Symbol; |
| 279 | + |
| 280 | + // Handle static field/property/method access like string.Empty or Console.WriteLine |
| 281 | + if (symbol is IFieldSymbol { IsStatic: true } or |
| 282 | + IPropertySymbol { IsStatic: true } or |
| 283 | + IMethodSymbol { IsStatic: true }) |
| 284 | + { |
| 285 | + // Use ToDisplayString to get fully qualified member access |
| 286 | + var fqn = symbol.ToDisplayString(s_fullyQualifiedFormat); |
| 287 | + return SyntaxFactory.ParseExpression(fqn).WithTriviaFrom(node); |
| 288 | + } |
| 289 | + |
| 290 | + // For instance member access, just qualify the expression part |
| 291 | + return base.VisitMemberAccessExpression(node); |
| 292 | + } |
| 293 | + |
| 294 | + public override SyntaxNode? VisitInvocationExpression(InvocationExpressionSyntax node) |
| 295 | + { |
| 296 | + var symbolInfo = _semanticModel.GetSymbolInfo(node); |
| 297 | + |
| 298 | + // Check if this is an extension method call |
| 299 | + if (symbolInfo.Symbol is IMethodSymbol { IsExtensionMethod: true } method) |
| 300 | + { |
| 301 | + // Rewrite extension method call to explicit static call |
| 302 | + // e.g., list.ToImmutableArray() -> ImmutableArray.ToImmutableArray(list) |
| 303 | + var containingType = method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); |
| 304 | + var methodName = method.Name; |
| 305 | + |
| 306 | + // Get the receiver (the 'this' argument) from the member access |
| 307 | + ExpressionSyntax? receiver = null; |
| 308 | + if (node.Expression is MemberAccessExpressionSyntax memberAccess) |
| 309 | + { |
| 310 | + receiver = (ExpressionSyntax?)Visit(memberAccess.Expression); |
| 311 | + } |
| 312 | + |
| 313 | + // Build the argument list: receiver + original arguments |
| 314 | + var arguments = new List<ArgumentSyntax>(); |
| 315 | + if (receiver is not null) |
| 316 | + { |
| 317 | + arguments.Add(SyntaxFactory.Argument(receiver)); |
| 318 | + } |
| 319 | + foreach (var arg in node.ArgumentList.Arguments) |
| 320 | + { |
| 321 | + var visitedArg = (ArgumentSyntax?)Visit(arg) ?? arg; |
| 322 | + arguments.Add(visitedArg); |
| 323 | + } |
| 324 | + |
| 325 | + // Handle generic method type arguments |
| 326 | + SimpleNameSyntax methodNameSyntax; |
| 327 | + if (method.TypeArguments.Length > 0) |
| 328 | + { |
| 329 | + var typeArgs = method.TypeArguments |
| 330 | + .Select(t => SyntaxFactory.ParseTypeName(t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))) |
| 331 | + .ToArray(); |
| 332 | + methodNameSyntax = SyntaxFactory.GenericName( |
| 333 | + SyntaxFactory.Identifier(methodName), |
| 334 | + SyntaxFactory.TypeArgumentList(SyntaxFactory.SeparatedList(typeArgs))); |
| 335 | + } |
| 336 | + else |
| 337 | + { |
| 338 | + methodNameSyntax = SyntaxFactory.IdentifierName(methodName); |
| 339 | + } |
| 340 | + |
| 341 | + // Build: ContainingType.MethodName(receiver, args...) |
| 342 | + var qualifiedMethod = SyntaxFactory.MemberAccessExpression( |
| 343 | + SyntaxKind.SimpleMemberAccessExpression, |
| 344 | + SyntaxFactory.ParseTypeName(containingType), |
| 345 | + methodNameSyntax); |
| 346 | + |
| 347 | + return SyntaxFactory.InvocationExpression( |
| 348 | + qualifiedMethod, |
| 349 | + SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(arguments))) |
| 350 | + .WithTriviaFrom(node); |
| 351 | + } |
| 352 | + |
| 353 | + return base.VisitInvocationExpression(node); |
| 354 | + } |
| 355 | + |
| 356 | + public override SyntaxNode? VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) |
| 357 | + { |
| 358 | + // If there's a complex initializer (collection init, etc.), don't preserve |
| 359 | + if (node.Initializer is not null) |
| 360 | + { |
| 361 | + Failed = true; |
| 362 | + return node; |
| 363 | + } |
| 364 | + |
| 365 | + var typeInfo = _semanticModel.GetTypeInfo(node); |
| 366 | + if (typeInfo.Type is INamedTypeSymbol namedType) |
| 367 | + { |
| 368 | + // Only preserve object creation if: |
| 369 | + // 1. It has arguments (constructor with params) |
| 370 | + // 2. It's a value type (structs always have parameterless constructors) |
| 371 | + // 3. It has a parameterless constructor |
| 372 | + bool hasArgs = node.ArgumentList?.Arguments.Count > 0; |
| 373 | + bool isValueType = namedType.IsValueType; |
| 374 | + bool hasParameterlessCtor = namedType.InstanceConstructors |
| 375 | + .Any(c => c.Parameters.Length == 0 && c.DeclaredAccessibility == Accessibility.Public); |
| 376 | + |
| 377 | + if (!hasArgs && !isValueType && !hasParameterlessCtor) |
| 378 | + { |
| 379 | + // Can't safely preserve this - the type doesn't have a parameterless constructor |
| 380 | + Failed = true; |
| 381 | + return node; |
| 382 | + } |
| 383 | + |
| 384 | + var fqn = namedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); |
| 385 | + var qualifiedType = SyntaxFactory.ParseTypeName(fqn) |
| 386 | + .WithLeadingTrivia(node.Type.GetLeadingTrivia()); |
| 387 | + |
| 388 | + var newArgs = node.ArgumentList is not null |
| 389 | + ? (ArgumentListSyntax?)Visit(node.ArgumentList) |
| 390 | + : null; |
| 391 | + |
| 392 | + return SyntaxFactory.ObjectCreationExpression( |
| 393 | + node.NewKeyword, |
| 394 | + qualifiedType, |
| 395 | + newArgs, |
| 396 | + null); |
| 397 | + } |
| 398 | + return base.VisitObjectCreationExpression(node); |
| 399 | + } |
| 400 | + |
| 401 | + public override SyntaxNode? VisitImplicitObjectCreationExpression(ImplicitObjectCreationExpressionSyntax node) |
| 402 | + { |
| 403 | + // Target-typed new like: new() { ... } or new(args) |
| 404 | + // These are complex to handle correctly - fail for now |
| 405 | + Failed = true; |
| 406 | + return node; |
| 407 | + } |
| 408 | + |
| 409 | + public override SyntaxNode? VisitImplicitArrayCreationExpression(ImplicitArrayCreationExpressionSyntax node) |
| 410 | + { |
| 411 | + // Implicit array like: new[] { 1, 2, 3 } is complex - don't preserve |
| 412 | + Failed = true; |
| 413 | + return node; |
| 414 | + } |
| 415 | + |
| 416 | + public override SyntaxNode? VisitGenericName(GenericNameSyntax node) |
| 417 | + { |
| 418 | + var symbolInfo = _semanticModel.GetSymbolInfo(node); |
| 419 | + if (symbolInfo.Symbol is ITypeSymbol typeSymbol) |
| 420 | + { |
| 421 | + var fqn = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); |
| 422 | + return SyntaxFactory.ParseTypeName(fqn).WithTriviaFrom(node); |
| 423 | + } |
| 424 | + return base.VisitGenericName(node); |
| 425 | + } |
| 426 | + |
| 427 | + public override SyntaxNode? VisitArrayCreationExpression(ArrayCreationExpressionSyntax node) |
| 428 | + { |
| 429 | + // Array creation with initializer is complex - don't preserve |
| 430 | + if (node.Initializer is not null) |
| 431 | + { |
| 432 | + Failed = true; |
| 433 | + return node; |
| 434 | + } |
| 435 | + return base.VisitArrayCreationExpression(node); |
| 436 | + } |
| 437 | + |
| 438 | + public override SyntaxNode? VisitTypeOfExpression(TypeOfExpressionSyntax node) |
| 439 | + { |
| 440 | + var typeInfo = _semanticModel.GetTypeInfo(node.Type); |
| 441 | + if (typeInfo.Type is not null) |
| 442 | + { |
| 443 | + var fqn = typeInfo.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); |
| 444 | + return SyntaxFactory.TypeOfExpression(SyntaxFactory.ParseTypeName(fqn)) |
| 445 | + .WithTriviaFrom(node); |
| 446 | + } |
| 447 | + return base.VisitTypeOfExpression(node); |
| 448 | + } |
| 449 | + |
| 450 | + public override SyntaxNode? VisitDefaultExpression(DefaultExpressionSyntax node) |
| 451 | + { |
| 452 | + if (node.Type is not null) |
| 453 | + { |
| 454 | + var typeInfo = _semanticModel.GetTypeInfo(node.Type); |
| 455 | + if (typeInfo.Type is not null) |
| 456 | + { |
| 457 | + var fqn = typeInfo.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); |
| 458 | + return SyntaxFactory.DefaultExpression(SyntaxFactory.ParseTypeName(fqn)) |
| 459 | + .WithTriviaFrom(node); |
| 460 | + } |
| 461 | + } |
| 462 | + return base.VisitDefaultExpression(node); |
| 463 | + } |
| 464 | + |
| 465 | + public override SyntaxNode? VisitCastExpression(CastExpressionSyntax node) |
| 466 | + { |
| 467 | + var typeInfo = _semanticModel.GetTypeInfo(node.Type); |
| 468 | + if (typeInfo.Type is not null) |
| 469 | + { |
| 470 | + var fqn = typeInfo.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); |
| 471 | + var newExpr = (ExpressionSyntax?)Visit(node.Expression) ?? node.Expression; |
| 472 | + return SyntaxFactory.CastExpression(SyntaxFactory.ParseTypeName(fqn), newExpr) |
| 473 | + .WithTriviaFrom(node); |
| 474 | + } |
| 475 | + return base.VisitCastExpression(node); |
| 476 | + } |
| 477 | + } |
188 | 478 | } |
189 | 479 | } |
0 commit comments