Skip to content

Commit 34bcf53

Browse files
committed
feat: Add DefineConstants supports for source file based build
1 parent 841fcdf commit 34bcf53

File tree

6 files changed

+130
-26
lines changed

6 files changed

+130
-26
lines changed

src/Docfx.Dotnet/CompilationHelper.cs

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
using CS = Microsoft.CodeAnalysis.CSharp;
99
using VB = Microsoft.CodeAnalysis.VisualBasic;
1010

11+
#nullable enable
12+
1113
namespace Docfx.Dotnet;
1214

1315
internal static class CompilationHelper
@@ -52,43 +54,55 @@ public static bool CheckDiagnostics(this Compilation compilation, bool errorAsWa
5254
return errorCount > 0;
5355
}
5456

55-
public static Compilation CreateCompilationFromCSharpFiles(IEnumerable<string> files)
57+
public static Compilation CreateCompilationFromCSharpFiles(IEnumerable<string> files, IDictionary<string, string> msbuildProperties)
5658
{
59+
var parserOption = GetCSharpParseOptions(msbuildProperties);
60+
var syntaxTrees = files.Select(path => CS.CSharpSyntaxTree.ParseText(File.ReadAllText(path), parserOption, path: path));
61+
5762
return CS.CSharpCompilation.Create(
5863
assemblyName: null,
5964
options: new CS.CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, xmlReferenceResolver: XmlFileResolver.Default),
60-
syntaxTrees: files.Select(path => CS.SyntaxFactory.ParseSyntaxTree(File.ReadAllText(path), path: path)),
65+
syntaxTrees: syntaxTrees,
6166
references: GetDefaultMetadataReferences("C#"));
6267
}
6368

64-
public static Compilation CreateCompilationFromCSharpCode(string code, string name = null, params MetadataReference[] references)
69+
public static Compilation CreateCompilationFromCSharpCode(string code, IDictionary<string, string> msbuildProperties, string? name = null, params MetadataReference[] references)
6570
{
71+
var parserOption = GetCSharpParseOptions(msbuildProperties);
72+
var syntaxTree = CS.CSharpSyntaxTree.ParseText(code, parserOption);
73+
6674
return CS.CSharpCompilation.Create(
6775
name,
6876
options: new CS.CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, xmlReferenceResolver: XmlFileResolver.Default),
69-
syntaxTrees: new[] { CS.SyntaxFactory.ParseSyntaxTree(code) },
77+
syntaxTrees: [syntaxTree],
7078
references: GetDefaultMetadataReferences("C#").Concat(references));
7179
}
7280

73-
public static Compilation CreateCompilationFromVBFiles(IEnumerable<string> files)
81+
public static Compilation CreateCompilationFromVBFiles(IEnumerable<string> files, IDictionary<string, string> msbuildProperties)
7482
{
83+
var parserOption = GetVisualBasicParseOptions(msbuildProperties);
84+
var syntaxTrees = files.Select(path => VB.VisualBasicSyntaxTree.ParseText(File.ReadAllText(path), parserOption, path: path));
85+
7586
return VB.VisualBasicCompilation.Create(
7687
assemblyName: null,
7788
options: new VB.VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary, globalImports: GetVBGlobalImports(), xmlReferenceResolver: XmlFileResolver.Default),
78-
syntaxTrees: files.Select(path => VB.SyntaxFactory.ParseSyntaxTree(File.ReadAllText(path), path: path)),
89+
syntaxTrees: syntaxTrees,
7990
references: GetDefaultMetadataReferences("VB"));
8091
}
8192

82-
public static Compilation CreateCompilationFromVBCode(string code, string name = null, params MetadataReference[] references)
93+
public static Compilation CreateCompilationFromVBCode(string code, IDictionary<string, string> msbuildProperties, string? name = null, params MetadataReference[] references)
8394
{
95+
var parserOption = GetVisualBasicParseOptions(msbuildProperties);
96+
var syntaxTree = VB.VisualBasicSyntaxTree.ParseText(code, parserOption);
97+
8498
return VB.VisualBasicCompilation.Create(
8599
name,
86100
options: new VB.VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary, globalImports: GetVBGlobalImports(), xmlReferenceResolver: XmlFileResolver.Default),
87-
syntaxTrees: new[] { VB.SyntaxFactory.ParseSyntaxTree(code) },
101+
syntaxTrees: [syntaxTree],
88102
references: GetDefaultMetadataReferences("VB").Concat(references));
89103
}
90104

91-
public static (Compilation, IAssemblySymbol) CreateCompilationFromAssembly(string assemblyPath, IEnumerable<string> references = null)
105+
public static (Compilation, IAssemblySymbol) CreateCompilationFromAssembly(string assemblyPath, IEnumerable<string>? references = null)
92106
{
93107
var metadataReference = CreateMetadataReference(assemblyPath);
94108
var compilation = CS.CSharpCompilation.Create(
@@ -100,7 +114,7 @@ public static (Compilation, IAssemblySymbol) CreateCompilationFromAssembly(strin
100114
.Select(CreateMetadataReference)
101115
.Append(metadataReference));
102116

103-
var assembly = (IAssemblySymbol)compilation.GetAssemblyOrModuleSymbol(metadataReference);
117+
var assembly = (IAssemblySymbol)compilation.GetAssemblyOrModuleSymbol(metadataReference)!;
104118
return (compilation, assembly);
105119
}
106120

@@ -124,8 +138,8 @@ private static IEnumerable<MetadataReference> GetDefaultMetadataReferences(strin
124138
{
125139
var dotnetExeDirectory = DotNetCorePathFinder.FindDotNetExeDirectory();
126140
var refDirectory = Path.Combine(dotnetExeDirectory, "packs/Microsoft.NETCore.App.Ref");
127-
var version = new DirectoryInfo(refDirectory).GetDirectories().Select(d => d.Name).Max();
128-
var moniker = new DirectoryInfo(Path.Combine(refDirectory, version, "ref")).GetDirectories().Select(d => d.Name).Max();
141+
var version = new DirectoryInfo(refDirectory).GetDirectories().Select(d => d.Name).Max()!;
142+
var moniker = new DirectoryInfo(Path.Combine(refDirectory, version, "ref")).GetDirectories().Select(d => d.Name).Max()!;
129143
var path = Path.Combine(refDirectory, version, "ref", moniker);
130144

131145
Logger.LogInfo($"Compiling {language} files using .NET SDK {version} for {moniker}");
@@ -177,4 +191,34 @@ private static MetadataReference CreateMetadataReference(string assemblyPath)
177191
var documentation = XmlDocumentationProvider.CreateFromFile(Path.ChangeExtension(assemblyPath, ".xml"));
178192
return MetadataReference.CreateFromFile(assemblyPath, documentation: documentation);
179193
}
194+
195+
private static CS.CSharpParseOptions GetCSharpParseOptions(IDictionary<string, string> msbuildProperties)
196+
{
197+
var preprocessorSymbols = (msbuildProperties.TryGetValue("DefineConstants", out var defineConstants))
198+
? defineConstants.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
199+
: null;
200+
201+
return new CS.CSharpParseOptions(preprocessorSymbols: preprocessorSymbols);
202+
}
203+
204+
private static VB.VisualBasicParseOptions GetVisualBasicParseOptions(IDictionary<string, string> msbuildProperties)
205+
{
206+
IEnumerable<KeyValuePair<string, object>>? preprocessorSymbols = null;
207+
if ((msbuildProperties.TryGetValue("DefineConstants", out var defineConstants)))
208+
{
209+
// Visual Basic use symbol/value pairs that are separated by semicolons. And are `key = value` pair syntax:
210+
// https://learn.microsoft.com/en-us/visualstudio/msbuild/vbc-task?view=vs-2022
211+
var items = defineConstants.Split(';');
212+
preprocessorSymbols = items.Select(x => x.Split('=', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
213+
.Where(x => x.Length == 2) // Silently ignore invalid formatted item.
214+
.Select(x => new KeyValuePair<string, object>(x[0].Trim(), x[1].Trim()))
215+
.ToArray();
216+
}
217+
else
218+
{
219+
preprocessorSymbols = null;
220+
}
221+
222+
return new VB.VisualBasicParseOptions(preprocessorSymbols: preprocessorSymbols);
223+
}
180224
}

src/Docfx.Dotnet/DotnetApiCatalog.Compile.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,14 @@ await LoadCompilationFromProject(project.AbsolutePath) is { } compilation)
8585

8686
if (files.TryGetValue(FileType.CSSourceCode, out var csFiles))
8787
{
88-
var compilation = CompilationHelper.CreateCompilationFromCSharpFiles(csFiles.Select(f => f.NormalizedPath));
88+
var compilation = CompilationHelper.CreateCompilationFromCSharpFiles(csFiles.Select(f => f.NormalizedPath), msbuildProperties);
8989
hasCompilationError |= compilation.CheckDiagnostics(config.AllowCompilationErrors);
9090
assemblies.Add((compilation.Assembly, compilation));
9191
}
9292

9393
if (files.TryGetValue(FileType.VBSourceCode, out var vbFiles))
9494
{
95-
var compilation = CompilationHelper.CreateCompilationFromVBFiles(vbFiles.Select(f => f.NormalizedPath));
95+
var compilation = CompilationHelper.CreateCompilationFromVBFiles(vbFiles.Select(f => f.NormalizedPath), msbuildProperties);
9696
hasCompilationError |= compilation.CheckDiagnostics(config.AllowCompilationErrors);
9797
assemblies.Add((compilation.Assembly, compilation));
9898
}

test/Docfx.Dotnet.Tests/ApiFilterUnitTest.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ namespace Docfx.Dotnet.Tests;
1010
[Collection("docfx STA")]
1111
public class ApiFilterUnitTest
1212
{
13+
private static readonly Dictionary<string, string> EmptyMSBuildProperties = new();
14+
1315
[Fact]
1416
public void TestApiFilter()
1517
{
@@ -365,9 +367,9 @@ public void TestExtendedSymbolKindFlags()
365367
Assert.True((ExtendedSymbolKind.Type | ExtendedSymbolKind.Member).Contains(new SymbolFilterData { Kind = ExtendedSymbolKind.Interface }));
366368
}
367369

368-
private static MetadataItem Verify(string code, ExtractMetadataConfig config = null, DotnetApiOptions options = null)
370+
private static MetadataItem Verify(string code, ExtractMetadataConfig config = null, DotnetApiOptions options = null, IDictionary<string, string> msbuildProperties = null)
369371
{
370-
var compilation = CompilationHelper.CreateCompilationFromCSharpCode(code, "test.dll");
372+
var compilation = CompilationHelper.CreateCompilationFromCSharpCode(code, msbuildProperties ?? EmptyMSBuildProperties, "test.dll");
371373
Assert.Empty(compilation.GetDeclarationDiagnostics());
372374
return compilation.Assembly.GenerateMetadataItem(compilation, config, options);
373375
}

test/Docfx.Dotnet.Tests/DefinitionMergeUnitTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public void Function<T>()
3131
3232
";
3333
// act
34-
var compilation = CompilationHelper.CreateCompilationFromCSharpCode(code, "test.dll");
34+
var compilation = CompilationHelper.CreateCompilationFromCSharpCode(code, msbuildProperties: new Dictionary<string, string>(), "test.dll");
3535
var output = compilation.Assembly.GenerateMetadataItem(compilation);
3636

3737
// assert

test/Docfx.Dotnet.Tests/GenerateMetadataFromCSUnitTest.cs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ namespace Docfx.Dotnet.Tests;
1212
[Collection("docfx STA")]
1313
public class GenerateMetadataFromCSUnitTest
1414
{
15-
private static MetadataItem Verify(string code, ExtractMetadataConfig config = null)
15+
private static readonly Dictionary<string, string> EmptyMSBuildProperties = new();
16+
17+
private static MetadataItem Verify(string code, ExtractMetadataConfig config = null, IDictionary<string, string> msbuildProperties = null)
1618
{
17-
var compilation = CompilationHelper.CreateCompilationFromCSharpCode(code, "test.dll");
19+
var compilation = CompilationHelper.CreateCompilationFromCSharpCode(code, msbuildProperties ?? EmptyMSBuildProperties, "test.dll");
1820
var extensionMethods = compilation.Assembly.FindExtensionMethods(new(new(), new())).ToArray();
1921
return compilation.Assembly.GenerateMetadataItem(compilation, config, extensionMethods: extensionMethods);
2022
}
@@ -2136,7 +2138,7 @@ public void Func1(Test1.Class1 i)
21362138
";
21372139
Directory.CreateDirectory(nameof(TestGenerateMetadataAsyncWithAssemblyInfoAndCrossReference));
21382140
var referencedAssembly = CreateAssemblyFromCSharpCode(referenceCode, $"{nameof(TestGenerateMetadataAsyncWithAssemblyInfoAndCrossReference)}/reference.dll");
2139-
var compilation = CreateCompilationFromCSharpCode(code, MetadataReference.CreateFromFile(referencedAssembly.Location));
2141+
var compilation = CreateCompilationFromCSharpCode(code, references: MetadataReference.CreateFromFile(referencedAssembly.Location));
21402142
Assert.Equal("test.dll", compilation.AssemblyName);
21412143
MetadataItem output = Verify(code);
21422144
Assert.Null(output.AssemblyNameList);
@@ -3104,9 +3106,9 @@ public class Foo
31043106
Assert.Equal("public IEnumerable<(string prefix, string uri)> Bar()", bar.Syntax.Content[SyntaxLanguage.CSharp]);
31053107
}
31063108

3107-
private static Compilation CreateCompilationFromCSharpCode(string code, params MetadataReference[] references)
3109+
private static Compilation CreateCompilationFromCSharpCode(string code, IDictionary<string, string> msbuildProperties = null, params MetadataReference[] references)
31083110
{
3109-
return CompilationHelper.CreateCompilationFromCSharpCode(code, "test.dll", references);
3111+
return CompilationHelper.CreateCompilationFromCSharpCode(code, msbuildProperties ?? EmptyMSBuildProperties, "test.dll", references);
31103112
}
31113113

31123114
private static Assembly CreateAssemblyFromCSharpCode(string code, string assemblyName)
@@ -3730,4 +3732,34 @@ public void F1() {}
37303732
Assert.Equal("public class Foo", foo.Syntax.Content[SyntaxLanguage.CSharp]);
37313733
Assert.Empty(foo.Items);
37323734
}
3735+
3736+
[Fact]
3737+
public void TestDefineConstantsMSBuildProperty()
3738+
{
3739+
var code =
3740+
"""
3741+
namespace Test
3742+
{
3743+
public class Foo
3744+
{
3745+
#if TEST
3746+
public void F1() {}
3747+
#endif
3748+
}
3749+
}
3750+
""";
3751+
3752+
// Test with DefineConstants
3753+
{
3754+
var output = Verify(code, msbuildProperties: new Dictionary<string, string> { ["DefineConstants"] = "TEST;DUMMY" });
3755+
var foo = output.Items[0].Items[0];
3756+
Assert.Equal("Test.Foo.F1", foo.Items[0].Name);
3757+
}
3758+
// Test without DefineConstants
3759+
{
3760+
var output = Verify(code, msbuildProperties: EmptyMSBuildProperties);
3761+
var foo = output.Items[0].Items[0];
3762+
Assert.Empty(foo.Items);
3763+
}
3764+
}
37333765
}

test/Docfx.Dotnet.Tests/GenerateMetadataFromVBUnitTest.cs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ namespace Docfx.Dotnet.Tests;
1111
[Collection("docfx STA")]
1212
public class GenerateMetadataFromVBUnitTest
1313
{
14-
private static MetadataItem Verify(string code, ExtractMetadataConfig options = null, params MetadataReference[] references)
14+
private static readonly Dictionary<string, string> EmptyMSBuildProperties = new();
15+
16+
private static MetadataItem Verify(string code, ExtractMetadataConfig options = null, IDictionary<string, string> msbuildProperties = null, params MetadataReference[] references)
1517
{
16-
var compilation = CompilationHelper.CreateCompilationFromVBCode(code, "test.dll", references);
18+
var compilation = CompilationHelper.CreateCompilationFromVBCode(code, msbuildProperties ?? EmptyMSBuildProperties, "test.dll", references);
1719
return compilation.Assembly.GenerateMetadataItem(compilation, options);
1820
}
1921

@@ -1613,8 +1615,32 @@ End Namespace
16131615
Assert.Equal("Public Function Bar() As IEnumerable(Of (prefix As String, uri As String))", bar.Syntax.Content[SyntaxLanguage.VB]);
16141616
}
16151617

1616-
private static Compilation CreateCompilationFromVBCode(string code, params MetadataReference[] references)
1618+
[Fact]
1619+
public void TestDefineConstantsMSBuildProperty()
16171620
{
1618-
return CompilationHelper.CreateCompilationFromVBCode(code, "test.dll", references);
1621+
var code =
1622+
"""
1623+
Namespace Test
1624+
Public Class Foo
1625+
#if TEST
1626+
Public Sub F1
1627+
#endif
1628+
End Function
1629+
End Class
1630+
End Namespace
1631+
""";
1632+
1633+
// Test with DefineConstants
1634+
{
1635+
var output = Verify(code, msbuildProperties: new Dictionary<string, string> { ["DefineConstants"] = "TEST=DUMMYVALUE;DUMMY=DUMMYVALUE" });
1636+
var foo = output.Items[0].Items[0];
1637+
Assert.Equal("Test.Foo.F1", foo.Items[0].Name);
1638+
}
1639+
// Test without DefineConstants
1640+
{
1641+
var output = Verify(code, msbuildProperties: EmptyMSBuildProperties);
1642+
var foo = output.Items[0].Items[0];
1643+
Assert.Empty(foo.Items);
1644+
}
16191645
}
16201646
}

0 commit comments

Comments
 (0)