Skip to content

Commit

Permalink
feat: Add DefineConstants supports for source file based build
Browse files Browse the repository at this point in the history
  • Loading branch information
filzrev committed Feb 20, 2024
1 parent 841fcdf commit 34bcf53
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 26 deletions.
68 changes: 56 additions & 12 deletions src/Docfx.Dotnet/CompilationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
using CS = Microsoft.CodeAnalysis.CSharp;
using VB = Microsoft.CodeAnalysis.VisualBasic;

#nullable enable

namespace Docfx.Dotnet;

internal static class CompilationHelper
Expand Down Expand Up @@ -52,43 +54,55 @@ public static bool CheckDiagnostics(this Compilation compilation, bool errorAsWa
return errorCount > 0;
}

public static Compilation CreateCompilationFromCSharpFiles(IEnumerable<string> files)
public static Compilation CreateCompilationFromCSharpFiles(IEnumerable<string> files, IDictionary<string, string> msbuildProperties)
{
var parserOption = GetCSharpParseOptions(msbuildProperties);
var syntaxTrees = files.Select(path => CS.CSharpSyntaxTree.ParseText(File.ReadAllText(path), parserOption, path: path));

return CS.CSharpCompilation.Create(
assemblyName: null,
options: new CS.CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, xmlReferenceResolver: XmlFileResolver.Default),
syntaxTrees: files.Select(path => CS.SyntaxFactory.ParseSyntaxTree(File.ReadAllText(path), path: path)),
syntaxTrees: syntaxTrees,
references: GetDefaultMetadataReferences("C#"));
}

public static Compilation CreateCompilationFromCSharpCode(string code, string name = null, params MetadataReference[] references)
public static Compilation CreateCompilationFromCSharpCode(string code, IDictionary<string, string> msbuildProperties, string? name = null, params MetadataReference[] references)
{
var parserOption = GetCSharpParseOptions(msbuildProperties);
var syntaxTree = CS.CSharpSyntaxTree.ParseText(code, parserOption);

return CS.CSharpCompilation.Create(
name,
options: new CS.CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, xmlReferenceResolver: XmlFileResolver.Default),
syntaxTrees: new[] { CS.SyntaxFactory.ParseSyntaxTree(code) },
syntaxTrees: [syntaxTree],
references: GetDefaultMetadataReferences("C#").Concat(references));
}

public static Compilation CreateCompilationFromVBFiles(IEnumerable<string> files)
public static Compilation CreateCompilationFromVBFiles(IEnumerable<string> files, IDictionary<string, string> msbuildProperties)
{
var parserOption = GetVisualBasicParseOptions(msbuildProperties);
var syntaxTrees = files.Select(path => VB.VisualBasicSyntaxTree.ParseText(File.ReadAllText(path), parserOption, path: path));

return VB.VisualBasicCompilation.Create(
assemblyName: null,
options: new VB.VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary, globalImports: GetVBGlobalImports(), xmlReferenceResolver: XmlFileResolver.Default),
syntaxTrees: files.Select(path => VB.SyntaxFactory.ParseSyntaxTree(File.ReadAllText(path), path: path)),
syntaxTrees: syntaxTrees,
references: GetDefaultMetadataReferences("VB"));
}

public static Compilation CreateCompilationFromVBCode(string code, string name = null, params MetadataReference[] references)
public static Compilation CreateCompilationFromVBCode(string code, IDictionary<string, string> msbuildProperties, string? name = null, params MetadataReference[] references)
{
var parserOption = GetVisualBasicParseOptions(msbuildProperties);
var syntaxTree = VB.VisualBasicSyntaxTree.ParseText(code, parserOption);

return VB.VisualBasicCompilation.Create(
name,
options: new VB.VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary, globalImports: GetVBGlobalImports(), xmlReferenceResolver: XmlFileResolver.Default),
syntaxTrees: new[] { VB.SyntaxFactory.ParseSyntaxTree(code) },
syntaxTrees: [syntaxTree],
references: GetDefaultMetadataReferences("VB").Concat(references));
}

public static (Compilation, IAssemblySymbol) CreateCompilationFromAssembly(string assemblyPath, IEnumerable<string> references = null)
public static (Compilation, IAssemblySymbol) CreateCompilationFromAssembly(string assemblyPath, IEnumerable<string>? references = null)
{
var metadataReference = CreateMetadataReference(assemblyPath);
var compilation = CS.CSharpCompilation.Create(
Expand All @@ -100,7 +114,7 @@ public static (Compilation, IAssemblySymbol) CreateCompilationFromAssembly(strin
.Select(CreateMetadataReference)
.Append(metadataReference));

var assembly = (IAssemblySymbol)compilation.GetAssemblyOrModuleSymbol(metadataReference);
var assembly = (IAssemblySymbol)compilation.GetAssemblyOrModuleSymbol(metadataReference)!;
return (compilation, assembly);
}

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

Logger.LogInfo($"Compiling {language} files using .NET SDK {version} for {moniker}");
Expand Down Expand Up @@ -177,4 +191,34 @@ private static MetadataReference CreateMetadataReference(string assemblyPath)
var documentation = XmlDocumentationProvider.CreateFromFile(Path.ChangeExtension(assemblyPath, ".xml"));
return MetadataReference.CreateFromFile(assemblyPath, documentation: documentation);
}

private static CS.CSharpParseOptions GetCSharpParseOptions(IDictionary<string, string> msbuildProperties)
{
var preprocessorSymbols = (msbuildProperties.TryGetValue("DefineConstants", out var defineConstants))
? defineConstants.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
: null;

return new CS.CSharpParseOptions(preprocessorSymbols: preprocessorSymbols);
}

private static VB.VisualBasicParseOptions GetVisualBasicParseOptions(IDictionary<string, string> msbuildProperties)
{
IEnumerable<KeyValuePair<string, object>>? preprocessorSymbols = null;
if ((msbuildProperties.TryGetValue("DefineConstants", out var defineConstants)))
{
// Visual Basic use symbol/value pairs that are separated by semicolons. And are `key = value` pair syntax:
// https://learn.microsoft.com/en-us/visualstudio/msbuild/vbc-task?view=vs-2022
var items = defineConstants.Split(';');
preprocessorSymbols = items.Select(x => x.Split('=', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
.Where(x => x.Length == 2) // Silently ignore invalid formatted item.
.Select(x => new KeyValuePair<string, object>(x[0].Trim(), x[1].Trim()))
.ToArray();
}
else
{
preprocessorSymbols = null;
}

return new VB.VisualBasicParseOptions(preprocessorSymbols: preprocessorSymbols);
}
}
4 changes: 2 additions & 2 deletions src/Docfx.Dotnet/DotnetApiCatalog.Compile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,14 @@ await LoadCompilationFromProject(project.AbsolutePath) is { } compilation)

if (files.TryGetValue(FileType.CSSourceCode, out var csFiles))
{
var compilation = CompilationHelper.CreateCompilationFromCSharpFiles(csFiles.Select(f => f.NormalizedPath));
var compilation = CompilationHelper.CreateCompilationFromCSharpFiles(csFiles.Select(f => f.NormalizedPath), msbuildProperties);
hasCompilationError |= compilation.CheckDiagnostics(config.AllowCompilationErrors);
assemblies.Add((compilation.Assembly, compilation));
}

if (files.TryGetValue(FileType.VBSourceCode, out var vbFiles))
{
var compilation = CompilationHelper.CreateCompilationFromVBFiles(vbFiles.Select(f => f.NormalizedPath));
var compilation = CompilationHelper.CreateCompilationFromVBFiles(vbFiles.Select(f => f.NormalizedPath), msbuildProperties);
hasCompilationError |= compilation.CheckDiagnostics(config.AllowCompilationErrors);
assemblies.Add((compilation.Assembly, compilation));
}
Expand Down
6 changes: 4 additions & 2 deletions test/Docfx.Dotnet.Tests/ApiFilterUnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ namespace Docfx.Dotnet.Tests;
[Collection("docfx STA")]
public class ApiFilterUnitTest
{
private static readonly Dictionary<string, string> EmptyMSBuildProperties = new();

[Fact]
public void TestApiFilter()
{
Expand Down Expand Up @@ -365,9 +367,9 @@ public void TestExtendedSymbolKindFlags()
Assert.True((ExtendedSymbolKind.Type | ExtendedSymbolKind.Member).Contains(new SymbolFilterData { Kind = ExtendedSymbolKind.Interface }));
}

private static MetadataItem Verify(string code, ExtractMetadataConfig config = null, DotnetApiOptions options = null)
private static MetadataItem Verify(string code, ExtractMetadataConfig config = null, DotnetApiOptions options = null, IDictionary<string, string> msbuildProperties = null)
{
var compilation = CompilationHelper.CreateCompilationFromCSharpCode(code, "test.dll");
var compilation = CompilationHelper.CreateCompilationFromCSharpCode(code, msbuildProperties ?? EmptyMSBuildProperties, "test.dll");
Assert.Empty(compilation.GetDeclarationDiagnostics());
return compilation.Assembly.GenerateMetadataItem(compilation, config, options);
}
Expand Down
2 changes: 1 addition & 1 deletion test/Docfx.Dotnet.Tests/DefinitionMergeUnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public void Function<T>()
";
// act
var compilation = CompilationHelper.CreateCompilationFromCSharpCode(code, "test.dll");
var compilation = CompilationHelper.CreateCompilationFromCSharpCode(code, msbuildProperties: new Dictionary<string, string>(), "test.dll");
var output = compilation.Assembly.GenerateMetadataItem(compilation);

// assert
Expand Down
42 changes: 37 additions & 5 deletions test/Docfx.Dotnet.Tests/GenerateMetadataFromCSUnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ namespace Docfx.Dotnet.Tests;
[Collection("docfx STA")]
public class GenerateMetadataFromCSUnitTest
{
private static MetadataItem Verify(string code, ExtractMetadataConfig config = null)
private static readonly Dictionary<string, string> EmptyMSBuildProperties = new();

private static MetadataItem Verify(string code, ExtractMetadataConfig config = null, IDictionary<string, string> msbuildProperties = null)
{
var compilation = CompilationHelper.CreateCompilationFromCSharpCode(code, "test.dll");
var compilation = CompilationHelper.CreateCompilationFromCSharpCode(code, msbuildProperties ?? EmptyMSBuildProperties, "test.dll");
var extensionMethods = compilation.Assembly.FindExtensionMethods(new(new(), new())).ToArray();
return compilation.Assembly.GenerateMetadataItem(compilation, config, extensionMethods: extensionMethods);
}
Expand Down Expand Up @@ -2136,7 +2138,7 @@ public void Func1(Test1.Class1 i)
";
Directory.CreateDirectory(nameof(TestGenerateMetadataAsyncWithAssemblyInfoAndCrossReference));
var referencedAssembly = CreateAssemblyFromCSharpCode(referenceCode, $"{nameof(TestGenerateMetadataAsyncWithAssemblyInfoAndCrossReference)}/reference.dll");
var compilation = CreateCompilationFromCSharpCode(code, MetadataReference.CreateFromFile(referencedAssembly.Location));
var compilation = CreateCompilationFromCSharpCode(code, references: MetadataReference.CreateFromFile(referencedAssembly.Location));
Assert.Equal("test.dll", compilation.AssemblyName);
MetadataItem output = Verify(code);
Assert.Null(output.AssemblyNameList);
Expand Down Expand Up @@ -3104,9 +3106,9 @@ public class Foo
Assert.Equal("public IEnumerable<(string prefix, string uri)> Bar()", bar.Syntax.Content[SyntaxLanguage.CSharp]);
}

private static Compilation CreateCompilationFromCSharpCode(string code, params MetadataReference[] references)
private static Compilation CreateCompilationFromCSharpCode(string code, IDictionary<string, string> msbuildProperties = null, params MetadataReference[] references)
{
return CompilationHelper.CreateCompilationFromCSharpCode(code, "test.dll", references);
return CompilationHelper.CreateCompilationFromCSharpCode(code, msbuildProperties ?? EmptyMSBuildProperties, "test.dll", references);
}

private static Assembly CreateAssemblyFromCSharpCode(string code, string assemblyName)
Expand Down Expand Up @@ -3730,4 +3732,34 @@ public void F1() {}
Assert.Equal("public class Foo", foo.Syntax.Content[SyntaxLanguage.CSharp]);
Assert.Empty(foo.Items);
}

[Fact]
public void TestDefineConstantsMSBuildProperty()
{
var code =
"""
namespace Test
{
public class Foo
{
#if TEST
public void F1() {}
#endif
}
}
""";

// Test with DefineConstants
{
var output = Verify(code, msbuildProperties: new Dictionary<string, string> { ["DefineConstants"] = "TEST;DUMMY" });
var foo = output.Items[0].Items[0];
Assert.Equal("Test.Foo.F1", foo.Items[0].Name);
}
// Test without DefineConstants
{
var output = Verify(code, msbuildProperties: EmptyMSBuildProperties);
var foo = output.Items[0].Items[0];
Assert.Empty(foo.Items);
}
}
}
34 changes: 30 additions & 4 deletions test/Docfx.Dotnet.Tests/GenerateMetadataFromVBUnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ namespace Docfx.Dotnet.Tests;
[Collection("docfx STA")]
public class GenerateMetadataFromVBUnitTest
{
private static MetadataItem Verify(string code, ExtractMetadataConfig options = null, params MetadataReference[] references)
private static readonly Dictionary<string, string> EmptyMSBuildProperties = new();

private static MetadataItem Verify(string code, ExtractMetadataConfig options = null, IDictionary<string, string> msbuildProperties = null, params MetadataReference[] references)
{
var compilation = CompilationHelper.CreateCompilationFromVBCode(code, "test.dll", references);
var compilation = CompilationHelper.CreateCompilationFromVBCode(code, msbuildProperties ?? EmptyMSBuildProperties, "test.dll", references);
return compilation.Assembly.GenerateMetadataItem(compilation, options);
}

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

private static Compilation CreateCompilationFromVBCode(string code, params MetadataReference[] references)
[Fact]
public void TestDefineConstantsMSBuildProperty()
{
return CompilationHelper.CreateCompilationFromVBCode(code, "test.dll", references);
var code =
"""
Namespace Test
Public Class Foo
#if TEST
Public Sub F1
#endif
End Function
End Class
End Namespace
""";

// Test with DefineConstants
{
var output = Verify(code, msbuildProperties: new Dictionary<string, string> { ["DefineConstants"] = "TEST=DUMMYVALUE;DUMMY=DUMMYVALUE" });
var foo = output.Items[0].Items[0];
Assert.Equal("Test.Foo.F1", foo.Items[0].Name);
}
// Test without DefineConstants
{
var output = Verify(code, msbuildProperties: EmptyMSBuildProperties);
var foo = output.Items[0].Items[0];
Assert.Empty(foo.Items);
}
}
}

0 comments on commit 34bcf53

Please sign in to comment.