Skip to content

Commit 0767268

Browse files
authored
Add multi-file functionality (for a single project) (#735)
This fixes almost all examples where multiple files are required, example for VersioningOfConstantsAndStaticReadonlyFields1/2, which actually require multiple *projects*. A few examples marked as requiring multiple files actually only needed additional files. It would be nice to be able to grab code from other examples to avoid maintenance notes, but that can be done later. Note that the additional files generated have no template, and therefore no implicit using directives - hence the addition of "using System.Diagnostics;" in a couple of examples. This is *slightly* unfortunate, but not a significant issue - and the alternative of specifying template files would be much more work. With this change in place, we will have 470 tested examples.
1 parent cf89ef0 commit 0767268

File tree

7 files changed

+112
-34
lines changed

7 files changed

+112
-34
lines changed

standard/attributes.md

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -611,9 +611,10 @@ It is important to understand that the inclusion or exclusion of a call to a con
611611
612612
> *Example*: In the following code
613613
>
614-
> <!-- RequiresSeparateFiles$Example: {template:"standalone-lib", name:"ConditionalMethods3", expectedOutput:["Executed Class1.F"]} -->
614+
> <!-- Example: {template:"standalone-lib", name:"ConditionalMethods3"} -->
615615
> ```csharp
616-
> // File class1.cs:
616+
> // File Class1.cs:
617+
> using System.Diagnostics;
617618
> class Class1
618619
> {
619620
> [Conditional("DEBUG")]
@@ -623,7 +624,7 @@ It is important to understand that the inclusion or exclusion of a call to a con
623624
> }
624625
> }
625626
>
626-
> // File class2.cs:
627+
> // File Class2.cs:
627628
> #define DEBUG
628629
> class Class2
629630
> {
@@ -633,7 +634,7 @@ It is important to understand that the inclusion or exclusion of a call to a con
633634
> }
634635
> }
635636
>
636-
> // File class3.cs:
637+
> // File Class3.cs:
637638
> #undef DEBUG
638639
> class Class3
639640
> {
@@ -652,16 +653,17 @@ The use of conditional methods in an inheritance chain can be confusing. Calls m
652653
653654
> *Example*: In the following code
654655
>
655-
> <!-- RequiresSeparateFiles$Example: {template:"standalone-lib", name:"ConditionalMethods4", expectedOutput:["Class2.M executed"]} -->
656+
> <!-- Example: {template:"standalone-console", name:"ConditionalMethods4", expectedOutput:["Class2.M executed"]} -->
656657
> ```csharp
657-
> // File class1.cs
658+
> // File Class1.cs
659+
> using System.Diagnostics;
658660
> class Class1
659661
> {
660662
> [Conditional("DEBUG")]
661663
> public virtual void M() => Console.WriteLine("Class1.M executed");
662664
> }
663665
>
664-
> // File class2.cs
666+
> // File Class2.cs
665667
> class Class2 : Class1
666668
> {
667669
> public override void M()
@@ -671,11 +673,11 @@ The use of conditional methods in an inheritance chain can be confusing. Calls m
671673
> }
672674
> }
673675
>
674-
> // File class3.cs
676+
> // File Class3.cs
675677
> #define DEBUG
676678
> class Class3
677679
> {
678-
> public static void Test()
680+
> public static void Main()
679681
> {
680682
> Class2 c = new Class2();
681683
> c.M(); // M is called
@@ -710,18 +712,19 @@ It is important to note that the inclusion or exclusion of an attribute specific
710712
711713
> *Example*: In the example
712714
>
713-
> <!-- RequiresSeparateFiles$Example: {template:"standalone-lib", name:"ConditionalAttributeClasses2"} -->
715+
> <!-- Example: {template:"standalone-lib", name:"ConditionalAttributeClasses2"} -->
714716
> ```csharp
715-
> // File test.cs:
717+
> // File Test.cs:
718+
> using System.Diagnostics;
716719
> [Conditional("DEBUG")]
717720
> public class TestAttribute : Attribute {}
718721
>
719-
> // File class1.cs:
722+
> // File Class1.cs:
720723
> #define DEBUG
721724
> [Test] // TestAttribute is specified
722725
> class Class1 {}
723726
>
724-
> // File class2.cs:
727+
> // File Class2.cs:
725728
> #undef DEBUG
726729
> [Test] // TestAttribute is not specified
727730
> class Class2 {}

standard/classes.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@ Nested types can be declared in multiple parts by using the `partial` modifier.
679679

680680
> *Example*: The following partial class is implemented in two parts, which reside in different compilation units. The first part is machine generated by a database-mapping tool while the second part is manually authored:
681681
>
682-
> <!-- RequiresSeparateFiles$Example: {template:"standalone-lib-without-using", name:"PartialDeclarations1", replaceEllipsis:true, expectedWarnings:["CS0169","CS0169","CS0169","CS0649"], additionalFiles:["Order.cs"]} -->
682+
> <!-- Example: {template:"standalone-lib-without-using", name:"PartialDeclarations1", replaceEllipsis:true, expectedWarnings:["CS0169","CS0169","CS0169","CS0649"], additionalFiles:["Order.cs"]} -->
683683
> ```csharp
684684
> public partial class Customer
685685
> {
@@ -694,6 +694,7 @@ Nested types can be declared in multiple parts by using the `partial` modifier.
694694
> }
695695
> }
696696
>
697+
> // File: Customer2.cs
697698
> public partial class Customer
698699
> {
699700
> public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);
@@ -1596,7 +1597,7 @@ Constants and readonly fields have different binary versioning semantics. When a
15961597
15971598
> *Example*: Consider an application that consists of two separate programs:
15981599
>
1599-
> <!-- RequiresSeparateFiles$Example: {template:"standalone-lib-without-using", name:"VersioningOfConstantsAndStaticReadonlyFields1"} -->
1600+
> <!-- RequiresSeparateProjects$Example: {template:"standalone-lib-without-using", name:"VersioningOfConstantsAndStaticReadonlyFields1"} -->
16001601
> ```csharp
16011602
> namespace Program1
16021603
> {
@@ -1609,7 +1610,7 @@ Constants and readonly fields have different binary versioning semantics. When a
16091610
>
16101611
> and
16111612
>
1612-
> <!-- RequiresSeparateFiles$Example: {template:"standalone-console", name:"VersioningOfConstantsAndStaticReadonlyFields2", expectedOutput:["x", "x", "x"], expectedErrors:["x","x"], expectedWarnings:["x","x"]} -->
1613+
> <!-- RequiresSeparateProjects$Example: {template:"standalone-console", name:"VersioningOfConstantsAndStaticReadonlyFields2", expectedOutput:["x", "x", "x"], expectedErrors:["x","x"], expectedWarnings:["x","x"]} -->
16131614
> ```csharp
16141615
> namespace Program2
16151616
> {
@@ -2793,20 +2794,17 @@ An implementing partial method declaration can appear in the same part as the co
27932794
27942795
Only a defining partial method participates in overload resolution. Thus, whether or not an implementing declaration is given, invocation expressions may resolve to invocations of the partial method. Because a partial method always returns `void`, such invocation expressions will always be expression statements. Furthermore, because a partial method is implicitly `private`, such statements will always occur within one of the parts of the type declaration within which the partial method is declared.
27952796
2796-
> *Note*: The definition of matching defining and implementing partial method declarations does not require parameter names to match. This can produce *surprising*, albeit *well defined*, behaviour when named arguments ([§11.6.2.1](expressions.md#11621-general)) are used. For example, given the defining partial method declaration for `M`:
2797+
> *Note*: The definition of matching defining and implementing partial method declarations does not require parameter names to match. This can produce *surprising*, albeit *well defined*, behaviour when named arguments ([§11.6.2.1](expressions.md#11621-general)) are used. For example, given the defining partial method declaration for `M` in one file, and the implementing partial method declaration in another file:
27972798
>
2798-
> <!-- RequiresSeparateFiles$Example: {template:"standalone-lib-without-using", name:"PartialMethods1"} -->
2799+
> <!-- Example: {template:"standalone-lib-without-using", name:"PartialMethods1", "expectedErrors":["CS1739"], "expectedWarnings":["CS8826"]} -->
27992800
> ```csharp
2801+
> // File P1.cs:
28002802
> partial class P
28012803
> {
28022804
> static partial void M(int x);
28032805
> }
2804-
> ```
2805-
>
2806-
> Then the implementing partial method declaration and invocation in other file:
28072806
>
2808-
> <!-- RequiresSeparateFiles$Example: {template:"standalone-lib-without-using", name:"PartialMethods2", expectedErrors:["x","x"], expectedWarnings:["x","x"]} -->
2809-
> ```csharp
2807+
> // File P2.cs:
28102808
> partial class P
28112809
> {
28122810
> static void Caller() => M(y: 0);
@@ -2841,6 +2839,7 @@ If a defining declaration but not an implementing declaration is given for a par
28412839
Partial methods are useful for allowing one part of a type declaration to customize the behavior of another part, e.g., one that is generated by a tool. Consider the following partial class declaration:
28422840
28432841
<!-- Example: {template:"standalone-lib-without-using", name:"PartialMethods3"} -->
2842+
<!-- Maintenance Note: This code exists in additional-files as "Customer.cs" to be used in an example below. As such, certain changes to this example should be reflected in that file, in which case, *all* examples using that file should be tested. -->
28442843
```csharp
28452844
partial class Customer
28462845
{
@@ -2880,7 +2879,7 @@ class Customer
28802879
28812880
Assume that another part is given, however, which provides implementing declarations of the partial methods:
28822881
2883-
<!-- RequiresSeparateFiles$Example: {template:"standalone-lib", name:"PartialMethods5", expectedErrors:["x","x"], expectedWarnings:["x","x"]} -->
2882+
<!-- Example: {template:"standalone-lib", name:"PartialMethods5", additionalFiles:["Customer.cs"]} -->
28842883
```csharp
28852884
partial class Customer
28862885
{
@@ -2926,6 +2925,7 @@ When the first parameter of a method includes the `this` modifier, that method i
29262925
> *Example*: The following is an example of a static class that declares two extension methods:
29272926
>
29282927
> <!-- Example: {template:"standalone-lib", name:"ExtensionMethods1"} -->
2928+
> <!-- Maintenance Note: This code exists in additional-files as "Extensions.cs" to be used in the examples below. As such, certain changes to this example should be reflected in that file, in which case, *all* examples using that file should be tested. -->
29292929
> ```csharp
29302930
> public static class Extensions
29312931
> {
@@ -2950,7 +2950,7 @@ An extension method is a regular static method. In addition, where its enclosing
29502950
29512951
> *Example*: The following program uses the extension methods declared above:
29522952
>
2953-
> <!-- RequiresSeparateFiles$Example: {template:"standalone-console", name:"ExtensionMethods2", expectedOutput:["x", "x", "x"], expectedErrors:["x","x"], expectedWarnings:["x","x"]} -->
2953+
> <!-- Example: {template:"standalone-console", name:"ExtensionMethods2", additionalFiles:["Extensions.cs"], expectedOutput:["22", "333"]} -->
29542954
> ```csharp
29552955
> static class Program
29562956
> {
@@ -2967,7 +2967,7 @@ An extension method is a regular static method. In addition, where its enclosing
29672967
>
29682968
> The `Slice` method is available on the `string[]`, and the `ToInt32` method is available on `string`, because they have been declared as extension methods. The meaning of the program is the same as the following, using ordinary static method calls:
29692969
>
2970-
> <!-- RequiresSeparateFiles$Example: {template:"standalone-console", name:"ExtensionMethods3", expectedOutput:["x", "x", "x"], expectedErrors:["x","x"], expectedWarnings:["x","x"]} -->
2970+
> <!-- Example: {template:"standalone-console", name:"ExtensionMethods3", additionalFiles:["Extensions.cs"], expectedOutput:["22", "333"]} -->
29712971
> ```csharp
29722972
> static class Program
29732973
> {

standard/namespaces.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ The *namespace_member_declaration*s of each compilation unit of a program contri
3232
> <!-- Example: {template:"standalone-lib-without-using", name:"CompilationUnits"} -->
3333
> ```csharp
3434
> // File A.cs:
35-
> class A {}
35+
> class A {}
3636
> // File B.cs:
37-
> class B {}
37+
> class B {}
3838
> ```
3939
>
4040
> The two compilation units contribute to the single global namespace, in this case declaring two classes with the fully qualified names `A` and `B`. Because the two compilation units contribute to the same declaration space, it would have been an error if each contained a declaration of a member with the same name.

tools/ExampleExtractor/Template.cs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ internal class Template
77
private const string AdditionalFilesDirectory = "additional-files";
88
private const string ExampleCodeSubstitution = "$example-code";
99
private const string ExampleNameSubstitution = "$example-name";
10+
private const string FileCommentPrefix = "// File ";
1011

1112
internal string Name { get; }
1213
private readonly Dictionary<string, string> files;
@@ -27,13 +28,16 @@ internal void Apply(Example example, string rootOutputDirectory, string rootTemp
2728
{
2829
var outputDirectory = Path.Combine(rootOutputDirectory, example.Name);
2930
Directory.CreateDirectory(outputDirectory);
31+
32+
var code = ExtractExtraFiles(example.Code, outputDirectory);
33+
3034
foreach (var pair in files)
3135
{
3236
string file = Path.Combine(outputDirectory, pair.Key);
33-
string code = pair.Value
34-
.Replace(ExampleCodeSubstitution, example.Code)
37+
string content = pair.Value
38+
.Replace(ExampleCodeSubstitution, code)
3539
.Replace(ExampleNameSubstitution, example.Name);
36-
File.WriteAllText(file, code);
40+
File.WriteAllText(file, content);
3741
}
3842
if (example.Metadata.AdditionalFiles is List<string> additionalFiles)
3943
{
@@ -48,6 +52,43 @@ internal void Apply(Example example, string rootOutputDirectory, string rootTemp
4852
File.WriteAllText(Path.Combine(outputDirectory, ExampleMetadata.MetadataFile), metadataJson);
4953
}
5054

55+
/// <summary>
56+
/// Returns all the code before the first "// File:" comment, extracting any additional
57+
/// files into the given directory.
58+
/// </summary>
59+
private string ExtractExtraFiles(string code, string outputDirectory)
60+
{
61+
var lines = code.Split('\n').ToList();
62+
// The implementation is a lot simpler if we know we've got an end marker.
63+
lines.Add(FileCommentPrefix + "IgnoreMe");
64+
string? currentFile = null;
65+
List<string> currentLines = new List<string>();
66+
string? initialCode = null;
67+
68+
foreach (var line in lines)
69+
{
70+
if (line.StartsWith(FileCommentPrefix))
71+
{
72+
if (currentFile is null)
73+
{
74+
// Remember this for later.
75+
initialCode = string.Join("\n", currentLines);
76+
}
77+
else
78+
{
79+
File.WriteAllLines(currentFile, currentLines);
80+
}
81+
currentLines.Clear();
82+
currentFile = Path.Combine(outputDirectory, line[FileCommentPrefix.Length..].TrimEnd(':'));
83+
}
84+
else
85+
{
86+
currentLines.Add(line);
87+
}
88+
}
89+
return initialCode ?? throw new InvalidOperationException($"Never saw a file terminator - bug in {nameof(ExtractExtraFiles)}");
90+
}
91+
5192
internal static Dictionary<string, Template> LoadTemplates(string directory) =>
5293
Directory.GetDirectories(directory)
5394
.Select(LoadTemplate)

tools/ExampleTester/GeneratedExample.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ private static GeneratedExample Load(string directory)
3535

3636
internal async Task<bool> Test(TesterConfiguration configuration)
3737
{
38-
var outputLines = new List<string>();
39-
outputLines.Add($"Testing {Metadata.Name} from {Metadata.Source}");
38+
var outputLines = new List<string> { $"Testing {Metadata.Name} from {Metadata.Source}" };
4039

41-
using var workspace = MSBuildWorkspace.Create();
40+
// Explicitly do a release build, to avoid implicitly defining DEBUG.
41+
var properties = new Dictionary<string, string> { { "Configuration", "Release" } };
42+
using var workspace = MSBuildWorkspace.Create(properties);
4243
// TODO: Validate this more cleanly.
4344
var projectFile = Metadata.Project is string specifiedProject
4445
? Path.Combine(directory, $"{specifiedProject}.csproj")
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
partial class Customer
2+
{
3+
string name;
4+
5+
public string Name
6+
{
7+
get => name;
8+
set
9+
{
10+
OnNameChanging(value);
11+
name = value;
12+
OnNameChanged();
13+
}
14+
}
15+
16+
partial void OnNameChanging(string newName);
17+
partial void OnNameChanged();
18+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
public static class Extensions
2+
{
3+
public static int ToInt32(this string s) => Int32.Parse(s);
4+
5+
public static T[] Slice<T>(this T[] source, int index, int count)
6+
{
7+
if (index < 0 || count < 0 || source.Length - index < count)
8+
{
9+
throw new ArgumentException();
10+
}
11+
T[] result = new T[count];
12+
Array.Copy(source, index, result, 0, count);
13+
return result;
14+
}
15+
}

0 commit comments

Comments
 (0)