1 module boilerplate.autostring; 2 3 import std.format : format; 4 import std.json; 5 import std.meta : Alias; 6 import std.traits : Unqual; 7 8 version(unittest) 9 { 10 import std.conv : to; 11 import std.datetime : SysTime; 12 import unit_threaded.should; 13 } 14 15 /++ 16 GenerateToString is a mixin string that automatically generates toString functions, 17 both sink-based and classic, customizable with UDA annotations on classes, members and functions. 18 +/ 19 public enum string GenerateToString = ` 20 import boilerplate.autostring : GenerateToStringTemplate; 21 static import std.json; 22 mixin GenerateToStringTemplate; 23 mixin(typeof(this).generateToStringErrCheck()); 24 mixin(typeof(this).generateToStringImpl()); 25 `; 26 27 /++ 28 When used with objects, toString methods of type string toString() are also created. 29 +/ 30 @("generates legacy toString on objects") 31 unittest 32 { 33 class Class 34 { 35 mixin(GenerateToString); 36 } 37 38 (new Class).to!string.shouldEqual("Class()"); 39 (new Class).toString.shouldEqual("Class()"); 40 (new Class).toJson.shouldEqual(`{}`.parseJSON); 41 } 42 43 /++ 44 A trailing underline in member names is removed when labeling. 45 +/ 46 @("removes trailing underline") 47 unittest 48 { 49 struct Struct 50 { 51 int a_; 52 mixin(GenerateToString); 53 } 54 55 Struct.init.to!string.shouldEqual("Struct(a=0)"); 56 Struct.init.toJson.shouldEqual(`{"a": 0}`.parseJSON); 57 } 58 59 /++ 60 The `@(ToString.Exclude)` tag can be used to exclude a member. 61 +/ 62 @("can exclude a member") 63 unittest 64 { 65 struct Struct 66 { 67 @(ToString.Exclude) 68 int a; 69 mixin(GenerateToString); 70 } 71 72 Struct.init.to!string.shouldEqual("Struct()"); 73 Struct.init.toJson.shouldEqual(`{}`.parseJSON); 74 } 75 76 /++ 77 The `@(ToString.Optional)` tag can be used to include a member only if it's in some form "present". 78 This means non-empty for arrays, non-null for objects, non-zero for ints. 79 +/ 80 @("can optionally exclude member") 81 unittest 82 { 83 import std.typecons : Nullable, nullable; 84 85 class Class 86 { 87 mixin(GenerateToString); 88 } 89 90 struct Test // some type that is not comparable to null or 0 91 { 92 mixin(GenerateToString); 93 } 94 95 struct Struct 96 { 97 @(ToString.Optional) 98 int a; 99 100 @(ToString.Optional) 101 string s; 102 103 @(ToString.Optional) 104 Class obj; 105 106 @(ToString.Optional) 107 Nullable!Test nullable; 108 109 mixin(GenerateToString); 110 } 111 112 Struct.init.to!string.shouldEqual("Struct()"); 113 Struct(2, "hi", new Class, Test().nullable).to!string 114 .shouldEqual(`Struct(a=2, s="hi", obj=Class(), nullable=Test())`); 115 Struct(2, "hi", new Class, Test().nullable).toJson 116 .shouldEqual(`{"a": 2, "s": "hi", "obj": {}, "nullable": {}}`.parseJSON); 117 Struct(0, "", null, Nullable!Test()).to!string.shouldEqual("Struct()"); 118 Struct(0, "", null, Nullable!Test()).toJson.shouldEqual(`{}`.parseJSON); 119 } 120 121 /++ 122 The `@(ToString.Optional)` tag can be used with a condition parameter 123 indicating when the type is to be _included._ 124 +/ 125 @("can pass exclusion condition to Optional") 126 unittest 127 { 128 struct Struct 129 { 130 @(ToString.Optional!(a => a > 3)) 131 int i; 132 133 mixin(GenerateToString); 134 } 135 136 Struct.init.to!string.shouldEqual("Struct()"); 137 Struct.init.toJson.shouldEqual(`{}`.parseJSON); 138 Struct(3).to!string.shouldEqual("Struct()"); 139 Struct(3).toJson.shouldEqual(`{}`.parseJSON); 140 Struct(5).to!string.shouldEqual("Struct(i=5)"); 141 Struct(5).toJson.shouldEqual(`{"i": 5}`.parseJSON); 142 } 143 144 /++ 145 The `@(ToString.Optional)` condition predicate 146 can also take the whole data type. 147 +/ 148 @("can pass exclusion condition to Optional") 149 unittest 150 { 151 struct Struct 152 { 153 @(ToString.Optional!(self => self.include)) 154 int i; 155 156 @(ToString.Exclude) 157 bool include; 158 159 mixin(GenerateToString); 160 } 161 162 Struct(5, false).to!string.shouldEqual("Struct()"); 163 Struct(5, false).toJson.shouldEqual(`{}`.parseJSON); 164 Struct(5, true).to!string.shouldEqual("Struct(i=5)"); 165 Struct(5, true).toJson.shouldEqual(`{"i": 5}`.parseJSON); 166 } 167 168 /++ 169 The `@(ToString.Include)` tag can be used to explicitly include a member. 170 This is intended to be used on property methods. 171 +/ 172 @("can include a method") 173 unittest 174 { 175 struct Struct 176 { 177 @(ToString.Include) 178 int foo() const { return 5; } 179 mixin(GenerateToString); 180 } 181 182 Struct.init.to!string.shouldEqual("Struct(foo=5)"); 183 } 184 185 /++ 186 The `@(ToString.Unlabeled)` tag will omit a field's name. 187 +/ 188 @("can omit names") 189 unittest 190 { 191 struct Struct 192 { 193 @(ToString.Unlabeled) 194 int a; 195 mixin(GenerateToString); 196 } 197 198 Struct.init.to!string.shouldEqual("Struct(0)"); 199 } 200 201 /++ 202 Parent class `toString()` methods are included automatically as the first entry, except if the parent class is `Object`. 203 +/ 204 @("can be used in both parent and child class") 205 unittest 206 { 207 class ParentClass { mixin(GenerateToString); } 208 209 class ChildClass : ParentClass { mixin(GenerateToString); } 210 211 (new ChildClass).to!string.shouldEqual("ChildClass(ParentClass())"); 212 } 213 214 @("invokes manually implemented parent toString") 215 unittest 216 { 217 class ParentClass 218 { 219 override string toString() const 220 { 221 return "Some string"; 222 } 223 } 224 225 class ChildClass : ParentClass { mixin(GenerateToString); } 226 227 (new ChildClass).to!string.shouldEqual("ChildClass(Some string)"); 228 } 229 230 @("invokes manually implemented parent toJson") 231 unittest 232 { 233 class ParentClass 234 { 235 JSONValue toJson() const 236 { 237 return JSONValue("test"); 238 } 239 } 240 241 class ChildClass : ParentClass { mixin(GenerateToString); } 242 243 (new ChildClass).toJson.shouldEqual(`"test"`.parseJSON); 244 } 245 246 @("can partially override toString in child class") 247 unittest 248 { 249 class ParentClass 250 { 251 mixin(GenerateToString); 252 } 253 254 class ChildClass : ParentClass 255 { 256 override string toString() const 257 { 258 return "Some string"; 259 } 260 261 mixin(GenerateToString); 262 } 263 264 (new ChildClass).to!string.shouldEqual("Some string"); 265 } 266 267 @("invokes manually implemented string toString in same class") 268 unittest 269 { 270 class Class 271 { 272 override string toString() const 273 { 274 return "Some string"; 275 } 276 277 mixin(GenerateToString); 278 } 279 280 (new Class).to!string.shouldEqual("Some string"); 281 } 282 283 @("invokes manually implemented void toString in same class") 284 unittest 285 { 286 class Class 287 { 288 void toString(scope void delegate(const(char)[]) sink) const 289 { 290 sink("Some string"); 291 } 292 293 mixin(GenerateToString); 294 } 295 296 (new Class).to!string.shouldEqual("Some string"); 297 } 298 299 /++ 300 Inclusion of parent class `toString()` can be prevented using `@(ToString.ExcludeSuper)`. 301 +/ 302 @("can suppress parent class toString()") 303 unittest 304 { 305 class ParentClass { } 306 307 @(ToString.ExcludeSuper) 308 class ChildClass : ParentClass { mixin(GenerateToString); } 309 310 (new ChildClass).to!string.shouldEqual("ChildClass()"); 311 } 312 313 /++ 314 The `@(ToString.Naked)` tag will omit the name of the type and parentheses. 315 +/ 316 @("can omit the type name") 317 unittest 318 { 319 @(ToString.Naked) 320 struct Struct 321 { 322 int a; 323 mixin(GenerateToString); 324 } 325 326 Struct.init.to!string.shouldEqual("a=0"); 327 } 328 329 /++ 330 Fields with the same name (ignoring capitalization) as their type, are unlabeled by default. 331 +/ 332 @("does not label fields with the same name as the type") 333 unittest 334 { 335 struct Struct1 { mixin(GenerateToString); } 336 337 struct Struct2 338 { 339 Struct1 struct1; 340 mixin(GenerateToString); 341 } 342 343 Struct2.init.to!string.shouldEqual("Struct2(Struct1())"); 344 } 345 346 @("does not label fields with the same name as the type, even if they're const") 347 unittest 348 { 349 struct Struct1 { mixin(GenerateToString); } 350 351 struct Struct2 352 { 353 const Struct1 struct1; 354 mixin(GenerateToString); 355 } 356 357 Struct2.init.to!string.shouldEqual("Struct2(Struct1())"); 358 } 359 360 @("does not label fields with the same name as the type, even if they're nullable") 361 unittest 362 { 363 import std.typecons : Nullable; 364 365 struct Struct1 { mixin(GenerateToString); } 366 367 struct Struct2 368 { 369 const Nullable!Struct1 struct1; 370 mixin(GenerateToString); 371 } 372 373 Struct2(Nullable!Struct1(Struct1())).to!string.shouldEqual("Struct2(Struct1())"); 374 } 375 376 /++ 377 This behavior can be prevented by explicitly tagging the field with `@(ToString.Labeled)`. 378 +/ 379 @("does label fields tagged as labeled") 380 unittest 381 { 382 struct Struct1 { mixin(GenerateToString); } 383 384 struct Struct2 385 { 386 @(ToString.Labeled) 387 Struct1 struct1; 388 mixin(GenerateToString); 389 } 390 391 Struct2.init.to!string.shouldEqual("Struct2(struct1=Struct1())"); 392 } 393 394 /++ 395 Fields of type 'SysTime' and name 'time' are unlabeled by default. 396 +/ 397 @("does not label SysTime time field correctly") 398 unittest 399 { 400 struct Struct { SysTime time; mixin(GenerateToString); } 401 402 Struct strct; 403 strct.time = SysTime.fromISOExtString("2003-02-01T11:55:00Z"); 404 405 // see unittest/config/string.d 406 strct.to!string.shouldEqual("Struct(2003-02-01T11:55:00Z)"); 407 } 408 409 /++ 410 Fields named 'id' are unlabeled only if they define their own toString(). 411 +/ 412 @("does not label id fields with toString()") 413 unittest 414 { 415 struct IdType 416 { 417 string toString() const { return "ID"; } 418 } 419 420 struct Struct 421 { 422 IdType id; 423 mixin(GenerateToString); 424 } 425 426 Struct.init.to!string.shouldEqual("Struct(ID)"); 427 } 428 429 /++ 430 Otherwise, they are labeled as normal. 431 +/ 432 @("labels id fields without toString") 433 unittest 434 { 435 struct Struct 436 { 437 int id; 438 mixin(GenerateToString); 439 } 440 441 Struct.init.to!string.shouldEqual("Struct(id=0)"); 442 } 443 444 /++ 445 Fields that are arrays with a name that is the pluralization of the array base type are also unlabeled by default, 446 as long as the array is NonEmpty. Otherwise, there would be no way to tell what the field contains. 447 +/ 448 @("does not label fields named a plural of the basetype, if the type is an array") 449 unittest 450 { 451 import boilerplate.conditions : NonEmpty; 452 453 struct Value { mixin(GenerateToString); } 454 struct Entity { mixin(GenerateToString); } 455 struct Day { mixin(GenerateToString); } 456 457 struct Struct 458 { 459 @NonEmpty 460 Value[] values; 461 462 @NonEmpty 463 Entity[] entities; 464 465 @NonEmpty 466 Day[] days; 467 468 mixin(GenerateToString); 469 } 470 471 auto value = Struct( 472 [Value()], 473 [Entity()], 474 [Day()]); 475 476 value.to!string.shouldEqual("Struct([Value()], [Entity()], [Day()])"); 477 } 478 479 @("does not label fields named a plural of the basetype, if the type is a BitFlags") 480 unittest 481 { 482 import std.typecons : BitFlags; 483 484 enum Flag 485 { 486 A = 1 << 0, 487 B = 1 << 1, 488 } 489 490 struct Struct 491 { 492 BitFlags!Flag flags; 493 494 mixin(GenerateToString); 495 } 496 497 auto value = Struct(BitFlags!Flag(Flag.A, Flag.B)); 498 499 value.to!string.shouldEqual("Struct(Flag(A, B))"); 500 } 501 502 /++ 503 Fields that are not NonEmpty are always labeled. 504 This is because they can be empty, in which case you can't tell what's in them from naming. 505 +/ 506 @("does label fields that may be empty") 507 unittest 508 { 509 import boilerplate.conditions : NonEmpty; 510 511 struct Value { mixin(GenerateToString); } 512 513 struct Struct 514 { 515 Value[] values; 516 517 mixin(GenerateToString); 518 } 519 520 Struct(null).to!string.shouldEqual("Struct(values=[])"); 521 } 522 523 /++ 524 `GenerateToString` can be combined with `GenerateFieldAccessors` without issue. 525 +/ 526 @("does not collide with accessors") 527 unittest 528 { 529 struct Struct 530 { 531 import boilerplate.accessors : ConstRead, GenerateFieldAccessors; 532 533 @ConstRead 534 private int a_; 535 536 mixin(GenerateFieldAccessors); 537 538 mixin(GenerateToString); 539 } 540 541 Struct.init.to!string.shouldEqual("Struct(a=0)"); 542 } 543 544 @("supports child classes of abstract classes") 545 unittest 546 { 547 static abstract class ParentClass 548 { 549 } 550 class ChildClass : ParentClass 551 { 552 mixin(GenerateToString); 553 } 554 } 555 556 @("supports custom toString handlers") 557 unittest 558 { 559 struct Struct 560 { 561 @ToStringHandler!(i => i ? "yes" : "no") 562 int i; 563 564 mixin(GenerateToString); 565 } 566 567 Struct.init.to!string.shouldEqual("Struct(i=no)"); 568 } 569 570 @("passes nullable unchanged to custom toString handlers") 571 unittest 572 { 573 import std.typecons : Nullable; 574 575 struct Struct 576 { 577 @ToStringHandler!(ni => ni.isNull ? "no" : "yes") 578 Nullable!int ni; 579 580 mixin(GenerateToString); 581 } 582 583 Struct.init.to!string.shouldEqual("Struct(ni=no)"); 584 } 585 586 // see unittest.config.string 587 @("supports optional BitFlags in structs") 588 unittest 589 { 590 import std.typecons : BitFlags; 591 592 enum Enum 593 { 594 A = 1, 595 B = 2, 596 } 597 598 struct Struct 599 { 600 @(ToString.Optional) 601 BitFlags!Enum field; 602 603 mixin(GenerateToString); 604 } 605 606 Struct.init.to!string.shouldEqual("Struct()"); 607 } 608 609 version (DigitalMars) 610 { 611 @("prints hashmaps in deterministic order") 612 unittest 613 { 614 struct Struct 615 { 616 string[string] map; 617 618 mixin(GenerateToString); 619 } 620 621 bool foundCollision = false; 622 623 foreach (key1; ["opstop", "opsto"]) 624 { 625 enum key2 = "foo"; // collide 626 627 const first = Struct([key1: null, key2: null]); 628 string[string] backwardsHashmap; 629 630 backwardsHashmap[key2] = null; 631 backwardsHashmap[key1] = null; 632 633 const second = Struct(backwardsHashmap); 634 635 if (first.map.keys != second.map.keys) 636 { 637 foundCollision = true; 638 first.to!string.shouldEqual(second.to!string); 639 } 640 } 641 assert(foundCollision, "none of the listed keys caused a hash collision"); 642 } 643 } 644 645 @("applies custom formatters to types in hashmaps") 646 unittest 647 { 648 import std.datetime : SysTime; 649 650 struct Struct 651 { 652 SysTime[string] map; 653 654 mixin(GenerateToString); 655 } 656 657 const expected = "2003-02-01T11:55:00Z"; 658 const value = Struct(["foo": SysTime.fromISOExtString(expected)]); 659 660 value.to!string.shouldEqual(`Struct(map=["foo": ` ~ expected ~ `])`); 661 } 662 663 @("can format associative array of Nullable SysTime") 664 unittest 665 { 666 import std.datetime : SysTime; 667 import std.typecons : Nullable; 668 669 struct Struct 670 { 671 Nullable!SysTime[string] map; 672 673 mixin(GenerateToString); 674 } 675 676 const expected = `Struct(map=["foo": null])`; 677 const value = Struct(["foo": Nullable!SysTime()]); 678 679 value.to!string.shouldEqual(expected); 680 } 681 682 @("can format associative array of type that cannot be sorted") 683 unittest 684 { 685 struct Struct 686 { 687 mixin(GenerateToString); 688 } 689 690 struct Struct2 691 { 692 bool[Struct] hashmap; 693 694 mixin(GenerateToString); 695 } 696 697 const expected = `Struct2(hashmap=[])`; 698 const value = Struct2(null); 699 700 value.to!string.shouldEqual(expected); 701 } 702 703 @("labels nested types with fully qualified names") 704 unittest 705 { 706 import std.datetime : SysTime; 707 import std.typecons : Nullable; 708 709 struct Struct 710 { 711 struct Struct2 712 { 713 mixin(GenerateToString); 714 } 715 716 Struct2 struct2; 717 718 mixin(GenerateToString); 719 } 720 721 const expected = `Struct(Struct.Struct2())`; 722 const value = Struct(Struct.Struct2()); 723 724 value.to!string.shouldEqual(expected); 725 } 726 727 @("supports fully qualified names with quotes") 728 unittest 729 { 730 struct Struct(string s) 731 { 732 struct Struct2 733 { 734 mixin(GenerateToString); 735 } 736 737 Struct2 struct2; 738 739 mixin(GenerateToString); 740 } 741 742 const expected = `Struct!"foo"(Struct!"foo".Struct2())`; 743 const value = Struct!"foo"(Struct!"foo".Struct2()); 744 745 value.to!string.shouldEqual(expected); 746 } 747 748 @("optional-always null Nullable") 749 unittest 750 { 751 import std.typecons : Nullable; 752 753 struct Struct 754 { 755 @(ToString.Optional!(a => true)) 756 Nullable!int i; 757 758 mixin(GenerateToString); 759 } 760 761 Struct().to!string.shouldEqual("Struct(i=Nullable.null)"); 762 } 763 764 @("force-included null Nullable") 765 unittest 766 { 767 import std.typecons : Nullable; 768 769 struct Struct 770 { 771 @(ToString.Include) 772 Nullable!int i; 773 774 mixin(GenerateToString); 775 } 776 777 Struct().to!string.shouldEqual("Struct(i=Nullable.null)"); 778 } 779 780 // test for clean detection of Nullable 781 @("struct with isNull") 782 unittest 783 { 784 struct Inner 785 { 786 bool isNull() const { return false; } 787 788 mixin(GenerateToString); 789 } 790 791 struct Outer 792 { 793 Inner inner; 794 795 mixin(GenerateToString); 796 } 797 798 Outer().to!string.shouldEqual("Outer(Inner())"); 799 } 800 801 // regression 802 @("mutable struct with alias this of sink toString") 803 unittest 804 { 805 struct Inner 806 { 807 public void toString(scope void delegate(const(char)[]) sink) const 808 { 809 sink("Inner()"); 810 } 811 } 812 813 struct Outer 814 { 815 Inner inner; 816 817 alias inner this; 818 819 mixin(GenerateToString); 820 } 821 } 822 823 @("immutable struct with alias this of const toString") 824 unittest 825 { 826 struct Inner 827 { 828 string toString() const { return "Inner()"; } 829 } 830 831 immutable struct Outer 832 { 833 Inner inner; 834 835 alias inner this; 836 837 mixin(GenerateToString); 838 } 839 840 Outer().to!string.shouldEqual("Outer(Inner())"); 841 } 842 843 @("class with alias to struct") 844 unittest 845 { 846 struct A 847 { 848 mixin(GenerateToString); 849 } 850 851 class B 852 { 853 A a; 854 855 alias a this; 856 857 mixin(GenerateToString); 858 } 859 860 (new B).to!string.shouldEqual("B(A())"); 861 } 862 863 mixin template GenerateToStringTemplate() 864 { 865 // this is a separate function to reduce the 866 // "warning: unreachable code" spam that is falsely created from static foreach 867 private static generateToStringErrCheck() 868 { 869 if (!__ctfe) 870 { 871 return null; 872 } 873 874 import boilerplate.autostring : ToString, typeName; 875 import boilerplate.util : GenNormalMemberTuple; 876 import std.string : format; 877 878 bool udaIncludeSuper; 879 bool udaExcludeSuper; 880 881 foreach (uda; __traits(getAttributes, typeof(this))) 882 { 883 static if (is(typeof(uda) == ToString)) 884 { 885 switch (uda) 886 { 887 case ToString.IncludeSuper: udaIncludeSuper = true; break; 888 case ToString.ExcludeSuper: udaExcludeSuper = true; break; 889 default: break; 890 } 891 } 892 } 893 894 if (udaIncludeSuper && udaExcludeSuper) 895 { 896 return format!(`static assert(false, ` ~ 897 `"Contradictory tags on '" ~ %(%s%) ~ "': IncludeSuper and ExcludeSuper");`) 898 ([typeName!(typeof(this))]); 899 } 900 901 mixin GenNormalMemberTuple!true; 902 903 foreach (member; NormalMemberTuple) 904 { 905 alias overloads = __traits(getOverloads, typeof(this), member, true); 906 static if (overloads.length > 0) 907 { 908 alias symbol = overloads[0]; 909 } 910 else 911 { 912 mixin("alias symbol = typeof(this)." ~ member ~ ";"); 913 } 914 enum error = checkAttributeConsistency!(__traits(getAttributes, symbol)); 915 916 static if (error) 917 { 918 return format!error(member); 919 } 920 } 921 922 return ``; 923 } 924 925 private static generateToStringImpl() 926 { 927 if (!__ctfe) 928 { 929 return null; 930 } 931 932 import boilerplate.autostring : 933 hasOwnStringToString, hasOwnVoidToString, isMemberUnlabeledByDefault, ToString, typeName; 934 import boilerplate.conditions : NonEmpty; 935 import boilerplate.util : GenNormalMemberTuple, udaIndex; 936 import std.json : JSONValue; 937 import std.meta : Alias; 938 import std.string : endsWith, format, split, startsWith, strip; 939 import std.traits : BaseClassesTuple, getUDAs, Unqual; 940 import std.typecons : Nullable; 941 942 // synchronized without lock contention is basically free, so always do it 943 // TODO enable when https://issues.dlang.org/show_bug.cgi?id=18504 is fixed 944 enum synchronize = false && is(typeof(this) == class); 945 946 const constExample = typeof(this).init; 947 auto normalExample = typeof(this).init; 948 949 enum alreadyHaveStringToString = __traits(hasMember, typeof(this), "toString") 950 && is(typeof(normalExample.toString()) == string); 951 enum alreadyHaveUsableStringToString = alreadyHaveStringToString 952 && is(typeof(constExample.toString()) == string); 953 954 enum alreadyHaveVoidToString = __traits(hasMember, typeof(this), "toString") 955 && is(typeof(normalExample.toString((void delegate(const(char)[])).init)) == void); 956 enum alreadyHaveUsableVoidToString = alreadyHaveVoidToString 957 && is(typeof(constExample.toString((void delegate(const(char)[])).init)) == void); 958 959 enum isObject = is(typeof(this): Object); 960 961 static if (isObject) 962 { 963 enum userDefinedStringToString = hasOwnStringToString!(typeof(this), typeof(super)); 964 enum userDefinedVoidToString = hasOwnVoidToString!(typeof(this), typeof(super)); 965 enum userDefinedToJson = hasOwnToJson!(typeof(this), typeof(super)); 966 } 967 else 968 { 969 enum userDefinedStringToString = hasOwnStringToString!(typeof(this)); 970 enum userDefinedVoidToString = hasOwnVoidToString!(typeof(this)); 971 enum userDefinedToJson = hasOwnToJson!(typeof(this)); 972 } 973 974 enum bool generateToStringFunction = !userDefinedStringToString && !userDefinedVoidToString; 975 enum bool generateToJsonFunction = !userDefinedToJson; 976 977 static if (generateToStringFunction || generateToJsonFunction) 978 { 979 string toStringFunction = null; 980 string toJsonFunction = null; 981 982 bool nakedMode; 983 bool udaIncludeSuper; 984 bool udaExcludeSuper; 985 986 foreach (uda; __traits(getAttributes, typeof(this))) 987 { 988 static if (is(typeof(uda) == ToStringEnum)) 989 { 990 switch (uda) 991 { 992 case ToString.Naked: nakedMode = true; break; 993 case ToString.IncludeSuper: udaIncludeSuper = true; break; 994 case ToString.ExcludeSuper: udaExcludeSuper = true; break; 995 default: break; 996 } 997 } 998 } 999 1000 string NamePlusOpenParen = typeName!(typeof(this)) ~ "("; 1001 1002 version(AutoStringDebug) 1003 { 1004 toStringFunction ~= format!`pragma(msg, "%s %s");`(alreadyHaveStringToString, alreadyHaveVoidToString); 1005 } 1006 1007 static if (isObject 1008 && is(typeof(typeof(super).init.toString((void delegate(const(char)[])).init)) == void)) 1009 { 1010 toStringFunction ~= `override `; 1011 } 1012 static if (isObject && is(typeof(super.toJson()) == JSONValue)) 1013 { 1014 toJsonFunction ~= `override `; 1015 } 1016 1017 toStringFunction ~= `public void toString(scope void delegate(const(char)[]) sink) const {` 1018 ~ `import boilerplate.autostring: ToStringHandler;` 1019 ~ `import boilerplate.util: sinkWrite;` 1020 ~ `import std.traits: getUDAs;`; 1021 toJsonFunction ~= `public std.json.JSONValue toJson() const {` 1022 ~ `import boilerplate.util: toJsonValue;` 1023 ~ `import std.json: JSONValue;`; 1024 1025 static if (synchronize) 1026 { 1027 toStringFunction ~= `synchronized (this) { `; 1028 toJsonFunction ~= `synchronized (this) { `; 1029 } 1030 1031 if (!nakedMode) 1032 { 1033 toStringFunction ~= format!`sink(%(%s%));`([NamePlusOpenParen]); 1034 } 1035 1036 bool includeSuperToString = false; 1037 bool includeSuperToJson = false; 1038 1039 static if (isObject) 1040 { 1041 if (alreadyHaveUsableStringToString || alreadyHaveUsableVoidToString) 1042 { 1043 includeSuperToString = true; 1044 } 1045 if (__traits(hasMember, typeof(super), "toJson")) 1046 { 1047 includeSuperToJson = true; 1048 } 1049 } 1050 1051 if (udaIncludeSuper) 1052 { 1053 includeSuperToString = true; 1054 includeSuperToJson = true; 1055 } 1056 else if (udaExcludeSuper) 1057 { 1058 includeSuperToString = false; 1059 includeSuperToJson = false; 1060 } 1061 1062 static if (isObject) 1063 { 1064 if (includeSuperToString) 1065 { 1066 static if (!alreadyHaveUsableStringToString && !alreadyHaveUsableVoidToString) 1067 { 1068 return `static assert(false, ` 1069 ~ `"cannot include super class in GenerateToString: ` 1070 ~ `parent class has no usable toString!");`; 1071 } 1072 else { 1073 static if (alreadyHaveUsableVoidToString) 1074 { 1075 toStringFunction ~= `super.toString(sink);`; 1076 } 1077 else 1078 { 1079 toStringFunction ~= `sink(super.toString());`; 1080 } 1081 toStringFunction ~= `bool comma = true;`; 1082 } 1083 } 1084 else 1085 { 1086 toStringFunction ~= `bool comma = false;`; 1087 } 1088 if (includeSuperToJson) 1089 { 1090 toJsonFunction ~= `JSONValue result = super.toJson;`; 1091 } 1092 else 1093 { 1094 toJsonFunction ~= `JSONValue result = JSONValue((JSONValue[string]).init);`; 1095 } 1096 } 1097 else 1098 { 1099 toStringFunction ~= `bool comma = false;`; 1100 toJsonFunction ~= `JSONValue result = JSONValue((JSONValue[string]).init);`; 1101 } 1102 1103 toStringFunction ~= `{`; 1104 1105 mixin GenNormalMemberTuple!(true); 1106 1107 foreach (member; NormalMemberTuple) 1108 { 1109 alias overloads = __traits(getOverloads, typeof(this), member, true); 1110 static if (overloads.length > 0) 1111 { 1112 alias symbol = overloads[0]; 1113 } 1114 else 1115 { 1116 mixin("alias symbol = typeof(this)." ~ member ~ ";"); 1117 } 1118 enum udaInclude = udaIndex!(ToString.Include, __traits(getAttributes, symbol)) != -1; 1119 enum udaExclude = udaIndex!(ToString.Exclude, __traits(getAttributes, symbol)) != -1; 1120 enum udaLabeled = udaIndex!(ToString.Labeled, __traits(getAttributes, symbol)) != -1; 1121 enum udaUnlabeled = udaIndex!(ToString.Unlabeled, __traits(getAttributes, symbol)) != -1; 1122 enum udaOptional = udaIndex!(ToString.Optional, __traits(getAttributes, symbol)) != -1; 1123 enum udaToStringHandler = udaIndex!(ToStringHandler, __traits(getAttributes, symbol)) != -1; 1124 enum udaNonEmpty = udaIndex!(NonEmpty, __traits(getAttributes, symbol)) != -1; 1125 1126 // see std.traits.isFunction!() 1127 static if ( 1128 is(symbol == function) 1129 || is(typeof(symbol) == function) 1130 || (is(typeof(&symbol) U : U*) && is(U == function))) 1131 { 1132 enum isFunction = true; 1133 } 1134 else 1135 { 1136 enum isFunction = false; 1137 } 1138 1139 enum includeOverride = udaInclude || udaOptional; 1140 1141 enum includeMember = (!isFunction || includeOverride) && !udaExclude; 1142 1143 static if (includeMember) 1144 { 1145 string memberName = member; 1146 1147 if (memberName.endsWith("_")) 1148 { 1149 memberName = memberName[0 .. $ - 1]; 1150 } 1151 1152 bool labeled = true; 1153 1154 static if (udaUnlabeled) 1155 { 1156 labeled = false; 1157 } 1158 1159 if (isMemberUnlabeledByDefault!(Unqual!(typeof(symbol)))(memberName, udaNonEmpty)) 1160 { 1161 labeled = false; 1162 } 1163 1164 static if (udaLabeled) 1165 { 1166 labeled = true; 1167 } 1168 1169 string membervalue = `this.` ~ member; 1170 1171 bool escapeStrings = true; 1172 1173 static if (udaToStringHandler) 1174 { 1175 alias Handlers = getUDAs!(symbol, ToStringHandler); 1176 1177 static assert(Handlers.length == 1); 1178 1179 static if (__traits(compiles, Handlers[0].Handler(typeof(symbol).init))) 1180 { 1181 membervalue = `getUDAs!(this.` ~ member ~ `, ToStringHandler)[0].Handler(` 1182 ~ membervalue 1183 ~ `)`; 1184 1185 escapeStrings = false; 1186 } 1187 else 1188 { 1189 return `static assert(false, "cannot determine how to call ToStringHandler");`; 1190 } 1191 } 1192 1193 string readMemberValue = membervalue; 1194 string jsonMemberValue = `this.` ~ member; 1195 string conditionalWritestmt; // formatted with sink.sinkWrite(... readMemberValue ... ) 1196 1197 static if (udaOptional) 1198 { 1199 import std.array : empty; 1200 1201 enum optionalIndex = udaIndex!(ToString.Optional, __traits(getAttributes, symbol)); 1202 alias optionalUda = Alias!(__traits(getAttributes, symbol)[optionalIndex]); 1203 1204 static if (is(optionalUda == struct)) 1205 { 1206 alias pred = Alias!(__traits(getAttributes, symbol)[optionalIndex]).condition; 1207 static if (__traits(compiles, pred(typeof(this).init))) 1208 { 1209 conditionalWritestmt = format!q{ 1210 if (__traits(getAttributes, %s)[%s].condition(this)) { %%s } 1211 } (membervalue, optionalIndex); 1212 } 1213 else 1214 { 1215 conditionalWritestmt = format!q{ 1216 if (__traits(getAttributes, %s)[%s].condition(%s)) { %%s } 1217 } (membervalue, optionalIndex, membervalue); 1218 } 1219 } 1220 else static if (__traits(compiles, typeof(symbol).init.isNull)) 1221 { 1222 conditionalWritestmt = format!q{if (!%s.isNull) { %%s }} 1223 (membervalue); 1224 1225 static if (is(typeof(symbol) : Nullable!T, T)) 1226 { 1227 readMemberValue = membervalue ~ `.get`; 1228 jsonMemberValue = `this.` ~ member ~ `.get`; 1229 } 1230 } 1231 else static if (__traits(compiles, typeof(symbol).init.empty)) 1232 { 1233 conditionalWritestmt = format!q{import std.array : empty; if (!%s.empty) { %%s }} 1234 (membervalue); 1235 } 1236 else static if (__traits(compiles, typeof(symbol).init !is null)) 1237 { 1238 conditionalWritestmt = format!q{if (%s !is null) { %%s }} 1239 (membervalue); 1240 } 1241 else static if (__traits(compiles, typeof(symbol).init != 0)) 1242 { 1243 conditionalWritestmt = format!q{if (%s != 0) { %%s }} 1244 (membervalue); 1245 } 1246 else static if (__traits(compiles, { if (typeof(symbol).init) { } })) 1247 { 1248 conditionalWritestmt = format!q{if (%s) { %%s }} 1249 (membervalue); 1250 } 1251 else 1252 { 1253 return format!(`static assert(false, ` 1254 ~ `"don't know how to figure out whether %s is present.");`) 1255 (member); 1256 } 1257 } 1258 else 1259 { 1260 // Nullables (without handler, that aren't force-included) fall back to optional 1261 static if (!udaToStringHandler && !udaInclude && 1262 __traits(compiles, typeof(symbol).init.isNull)) 1263 { 1264 conditionalWritestmt = format!q{if (!%s.isNull) { %%s }} 1265 (membervalue); 1266 1267 static if (is(typeof(symbol) : Nullable!T, T)) 1268 { 1269 readMemberValue = membervalue ~ `.get`; 1270 jsonMemberValue = `this.` ~ member ~ `.get`; 1271 } 1272 } 1273 else 1274 { 1275 conditionalWritestmt = q{ %s }; 1276 } 1277 } 1278 1279 string writeStmt; 1280 1281 if (labeled) 1282 { 1283 writeStmt = format!`sink.sinkWrite(comma, %s, "%s=%%s", %s);` 1284 (escapeStrings, memberName, readMemberValue); 1285 } 1286 else 1287 { 1288 writeStmt = format!`sink.sinkWrite(comma, %s, "%%s", %s);` 1289 (escapeStrings, readMemberValue); 1290 } 1291 string writeJsonStmt = format!`result["%s"] = toJsonValue(%s);` 1292 (memberName, jsonMemberValue); 1293 1294 toStringFunction ~= format(conditionalWritestmt, writeStmt); 1295 toJsonFunction ~= format(conditionalWritestmt, writeJsonStmt); 1296 } 1297 } 1298 1299 toStringFunction ~= `} `; 1300 1301 if (!nakedMode) 1302 { 1303 toStringFunction ~= `sink(")");`; 1304 } 1305 1306 static if (synchronize) 1307 { 1308 toStringFunction ~= `} `; 1309 toJsonFunction ~= `} `; 1310 } 1311 1312 toStringFunction ~= `} `; 1313 toJsonFunction ~= `return result;` 1314 ~ `} `; 1315 } 1316 1317 static if (userDefinedStringToString && userDefinedVoidToString) 1318 { 1319 static assert(!generateToStringFunction); 1320 1321 string toStringCode = ``; // Nothing to be done. 1322 } 1323 // if the user has defined their own string toString() in this aggregate: 1324 else static if (userDefinedStringToString) 1325 { 1326 static assert(!generateToStringFunction); 1327 1328 // just call it. 1329 static if (alreadyHaveUsableStringToString) 1330 { 1331 string toStringCode = `public void toString(scope void delegate(const(char)[]) sink) const {` ~ 1332 ` sink(this.toString());` ~ 1333 ` }`; 1334 1335 static if (isObject 1336 && is(typeof(typeof(super).init.toString((void delegate(const(char)[])).init)) == void)) 1337 { 1338 toStringCode = `override ` ~ toStringCode; 1339 } 1340 } 1341 else 1342 { 1343 string toStringCode = `static assert(false, "toString is not const in this class.");`; 1344 } 1345 } 1346 // if the user has defined their own void toString() in this aggregate: 1347 else 1348 { 1349 1350 string toStringCode; 1351 1352 static if (!userDefinedVoidToString) 1353 { 1354 static assert(generateToStringFunction); 1355 1356 toStringCode = toStringFunction; 1357 } 1358 else 1359 { 1360 static assert(!generateToStringFunction); 1361 } 1362 1363 // generate fallback string toString() 1364 // that calls, specifically, *our own* toString impl. 1365 // (this is important to break cycles when a subclass implements a toString that calls super.toString) 1366 static if (isObject) 1367 { 1368 toStringCode ~= `override `; 1369 } 1370 1371 toStringCode ~= `public string toString() const {` 1372 ~ `string result;` 1373 ~ `typeof(this).toString((const(char)[] part) { result ~= part; });` 1374 ~ `return result;` 1375 ~ `}`; 1376 } 1377 static if (userDefinedToJson) 1378 { 1379 string toJsonCode = ``; 1380 } 1381 else 1382 { 1383 string toJsonCode = toJsonFunction; 1384 } 1385 return toStringCode ~ toJsonCode; 1386 } 1387 } 1388 1389 template checkAttributeConsistency(Attributes...) 1390 { 1391 enum checkAttributeConsistency = checkAttributeHelper(); 1392 1393 private string checkAttributeHelper() 1394 { 1395 if (!__ctfe) 1396 { 1397 return null; 1398 } 1399 1400 import std.string : format; 1401 1402 bool include, exclude, optional, labeled, unlabeled; 1403 1404 foreach (uda; Attributes) 1405 { 1406 static if (is(typeof(uda) == ToStringEnum)) 1407 { 1408 switch (uda) 1409 { 1410 case ToString.Include: include = true; break; 1411 case ToString.Exclude: exclude = true; break; 1412 case ToString.Labeled: labeled = true; break; 1413 case ToString.Unlabeled: unlabeled = true; break; 1414 default: break; 1415 } 1416 } 1417 else static if (is(uda == struct) && __traits(isSame, uda, ToString.Optional)) 1418 { 1419 optional = true; 1420 } 1421 } 1422 1423 if (include && exclude) 1424 { 1425 return `static assert(false, "Contradictory tags on '%s': Include and Exclude");`; 1426 } 1427 1428 if (include && optional) 1429 { 1430 return `static assert(false, "Redundant tags on '%s': Optional implies Include");`; 1431 } 1432 1433 if (exclude && optional) 1434 { 1435 return `static assert(false, "Contradictory tags on '%s': Exclude and Optional");`; 1436 } 1437 1438 if (labeled && unlabeled) 1439 { 1440 return `static assert(false, "Contradictory tags on '%s': Labeled and Unlabeled");`; 1441 } 1442 1443 return null; 1444 } 1445 } 1446 1447 struct ToStringHandler(alias Handler_) 1448 { 1449 alias Handler = Handler_; 1450 } 1451 1452 enum ToStringEnum 1453 { 1454 // these go on the class 1455 Naked, 1456 IncludeSuper, 1457 ExcludeSuper, 1458 1459 // these go on the field/method 1460 Unlabeled, 1461 Labeled, 1462 Exclude, 1463 Include, 1464 } 1465 1466 struct ToString 1467 { 1468 static foreach (name; __traits(allMembers, ToStringEnum)) 1469 { 1470 mixin(format!q{enum %s = ToStringEnum.%s;}(name, name)); 1471 } 1472 1473 static struct Optional(alias condition_) 1474 { 1475 alias condition = condition_; 1476 } 1477 } 1478 1479 public bool isMemberUnlabeledByDefault(Type)(string field, bool attribNonEmpty) 1480 { 1481 import std.datetime : SysTime; 1482 import std.range.primitives : ElementType, isInputRange; 1483 // Types whose toString starts with the contained type 1484 import std.typecons : BitFlags, Nullable; 1485 1486 field = field.toLower; 1487 1488 static if (is(Type: const Nullable!BaseType, BaseType)) 1489 { 1490 if (field == BaseType.stringof.toLower) 1491 { 1492 return true; 1493 } 1494 } 1495 else static if (isInputRange!Type) 1496 { 1497 alias BaseType = ElementType!Type; 1498 1499 if (field == BaseType.stringof.toLower.pluralize && attribNonEmpty) 1500 { 1501 return true; 1502 } 1503 } 1504 else static if (is(Type: const BitFlags!BaseType, BaseType)) 1505 { 1506 if (field == BaseType.stringof.toLower.pluralize) 1507 { 1508 return true; 1509 } 1510 } 1511 1512 return field == Type.stringof.toLower 1513 || (field == "time" && is(Type == SysTime)) 1514 || (field == "id" && is(typeof(Type.toString))); 1515 } 1516 1517 private string toLower(string text) 1518 { 1519 import std.string : stdToLower = toLower; 1520 1521 string result = null; 1522 1523 foreach (ub; cast(immutable(ubyte)[]) text) 1524 { 1525 if (ub >= 0x80) // utf-8, non-ascii 1526 { 1527 return text.stdToLower; 1528 } 1529 if (ub >= 'A' && ub <= 'Z') 1530 { 1531 result ~= cast(char) (ub + ('a' - 'A')); 1532 } 1533 else 1534 { 1535 result ~= cast(char) ub; 1536 } 1537 } 1538 return result; 1539 } 1540 1541 // http://code.activestate.com/recipes/82102/ 1542 private string pluralize(string label) 1543 { 1544 import std.algorithm.searching : contain = canFind; 1545 1546 string postfix = "s"; 1547 if (label.length > 2) 1548 { 1549 enum vowels = "aeiou"; 1550 1551 if (label.stringEndsWith("ch") || label.stringEndsWith("sh")) 1552 { 1553 postfix = "es"; 1554 } 1555 else if (auto before = label.stringEndsWith("y")) 1556 { 1557 if (!vowels.contain(label[$ - 2])) 1558 { 1559 postfix = "ies"; 1560 label = before; 1561 } 1562 } 1563 else if (auto before = label.stringEndsWith("is")) 1564 { 1565 postfix = "es"; 1566 label = before; 1567 } 1568 else if ("sxz".contain(label[$-1])) 1569 { 1570 postfix = "es"; // glasses 1571 } 1572 } 1573 return label ~ postfix; 1574 } 1575 1576 @("has functioning pluralize()") 1577 unittest 1578 { 1579 "dog".pluralize.shouldEqual("dogs"); 1580 "ash".pluralize.shouldEqual("ashes"); 1581 "day".pluralize.shouldEqual("days"); 1582 "entity".pluralize.shouldEqual("entities"); 1583 "thesis".pluralize.shouldEqual("theses"); 1584 "glass".pluralize.shouldEqual("glasses"); 1585 } 1586 1587 private string stringEndsWith(const string text, const string suffix) 1588 { 1589 import std.range : dropBack; 1590 import std.string : endsWith; 1591 1592 if (text.endsWith(suffix)) 1593 { 1594 return text.dropBack(suffix.length); 1595 } 1596 return null; 1597 } 1598 1599 @("has functioning stringEndsWith()") 1600 unittest 1601 { 1602 "".stringEndsWith("").shouldNotBeNull; 1603 "".stringEndsWith("x").shouldBeNull; 1604 "Hello".stringEndsWith("Hello").shouldNotBeNull; 1605 "Hello".stringEndsWith("Hello").shouldEqual(""); 1606 "Hello".stringEndsWith("lo").shouldEqual("Hel"); 1607 } 1608 1609 template hasOwnFunction(Aggregate, Super, string name, Ret) 1610 if (!__traits(hasMember, Aggregate, name)) 1611 { 1612 enum hasOwnFunction = false; 1613 } 1614 1615 template hasOwnFunction(Aggregate, Super, string name, Ret) 1616 if (__traits(hasMember, Aggregate, name)) 1617 { 1618 import std.meta : AliasSeq, Filter; 1619 import std.traits : ReturnType, Unqual; 1620 1621 enum FunctionMatchesType(alias Fun) = is(Unqual!(ReturnType!Fun) == Ret); 1622 1623 alias MyFunctions = AliasSeq!(__traits(getOverloads, Aggregate, name)); 1624 alias MatchingFunctions = Filter!(FunctionMatchesType, MyFunctions); 1625 enum hasFunction = MatchingFunctions.length == 1; 1626 1627 static if (__traits(hasMember, Super, name)) 1628 { 1629 alias SuperFunctions = AliasSeq!(__traits(getOverloads, Super, name)); 1630 } 1631 else 1632 { 1633 alias SuperFunctions = AliasSeq!(); 1634 } 1635 alias SuperMatchingFunctions = Filter!(FunctionMatchesType, SuperFunctions); 1636 enum superHasFunction = SuperMatchingFunctions.length == 1; 1637 1638 static if (hasFunction) 1639 { 1640 static if (superHasFunction) 1641 { 1642 enum hasOwnFunction = !__traits(isSame, MatchingFunctions[0], SuperMatchingFunctions[0]); 1643 } 1644 else 1645 { 1646 enum hasOwnFunction = true; 1647 } 1648 } 1649 else 1650 { 1651 enum hasOwnFunction = false; 1652 } 1653 } 1654 1655 /** 1656 * Find qualified name of `T` including any containing types; not including containing functions or modules. 1657 */ 1658 public template typeName(T) 1659 { 1660 static if (__traits(compiles, __traits(parent, T))) 1661 { 1662 alias parent = Alias!(__traits(parent, T)); 1663 enum isSame = __traits(isSame, T, parent); 1664 1665 static if (!isSame && ( 1666 is(parent == struct) || is(parent == union) || is(parent == enum) || 1667 is(parent == class) || is(parent == interface))) 1668 { 1669 enum typeName = typeName!parent ~ "." ~ Unqual!T.stringof; 1670 } 1671 else 1672 { 1673 enum typeName = Unqual!T.stringof; 1674 } 1675 } 1676 else 1677 { 1678 enum typeName = Unqual!T.stringof; 1679 } 1680 } 1681 1682 public alias hasOwnStringToString(T...) = hasOwn!(T, "toString", string); 1683 public alias hasOwnVoidToString(T...) = hasOwn!(T, "toString", void); 1684 public alias hasOwnToJson(T...) = hasOwn!(T, "toJson", JSONValue); 1685 1686 private template hasOwn(Aggregate, Super, string name, Ret) 1687 if (is(Aggregate: Object)) 1688 { 1689 enum hasOwn = hasOwnFunction!(Aggregate, Super, name, Ret); 1690 } 1691 1692 private template hasOwn(Aggregate, string name, Ret) 1693 if (is(Aggregate == struct)) 1694 { 1695 import std.traits : ReturnType; 1696 1697 static if (is(ReturnType!(__traits(getMember, Aggregate.init, name)) == Ret)) 1698 { 1699 enum hasOwn = !isFromAliasThis!(Aggregate, name, Ret); 1700 } 1701 else 1702 { 1703 enum hasOwn = false; 1704 } 1705 } 1706 1707 public template isFromAliasThis(T, string member, Type) 1708 { 1709 import std.meta : AliasSeq, anySatisfy, Filter; 1710 1711 enum FunctionMatchesType(alias Fun) = is(Unqual!(typeof(Fun)) == Type); 1712 1713 private template isFromThatAliasThis(string field) 1714 { 1715 alias aliasMembers = AliasSeq!(__traits(getOverloads, __traits(getMember, T.init, field), member)); 1716 alias ownMembers = AliasSeq!(__traits(getOverloads, T, member)); 1717 1718 enum bool isFromThatAliasThis = __traits(isSame, 1719 Filter!(FunctionMatchesType, aliasMembers), 1720 Filter!(FunctionMatchesType, ownMembers)); 1721 } 1722 1723 enum bool isFromAliasThis = anySatisfy!(isFromThatAliasThis, __traits(getAliasThis, T)); 1724 } 1725 1726 @("correctly recognizes the existence of string toString() in a class") 1727 unittest 1728 { 1729 class Class1 1730 { 1731 override string toString() { return null; } 1732 static assert(!hasOwnVoidToString!(typeof(this), typeof(super))); 1733 static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1734 } 1735 1736 class Class2 1737 { 1738 override string toString() const { return null; } 1739 static assert(!hasOwnVoidToString!(typeof(this), typeof(super))); 1740 static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1741 } 1742 1743 class Class3 1744 { 1745 void toString(scope void delegate(const(char)[]) sink) const { } 1746 override string toString() const { return null; } 1747 static assert(hasOwnVoidToString!(typeof(this), typeof(super))); 1748 static assert(hasOwnStringToString!(typeof(this), typeof(super))); 1749 } 1750 1751 class Class4 1752 { 1753 void toString(scope void delegate(const(char)[]) sink) const { } 1754 static assert(hasOwnVoidToString!(typeof(this), typeof(super))); 1755 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1756 } 1757 1758 class Class5 1759 { 1760 mixin(GenerateToString); 1761 } 1762 1763 class ChildClass1 : Class1 1764 { 1765 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1766 } 1767 1768 class ChildClass2 : Class2 1769 { 1770 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1771 } 1772 1773 class ChildClass3 : Class3 1774 { 1775 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1776 } 1777 1778 class ChildClass5 : Class5 1779 { 1780 static assert(!hasOwnStringToString!(typeof(this), typeof(super))); 1781 } 1782 }