Skip to content

Commit

Permalink
feat: add support for global usings
Browse files Browse the repository at this point in the history
  • Loading branch information
rdavisau committed Apr 20, 2022
1 parent 93a8f01 commit 9f9167c
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 4 deletions.
29 changes: 28 additions & 1 deletion readme.md
Expand Up @@ -122,7 +122,7 @@ VS for Mac seems to like to show the break higher in the callstack (at the first

## source generators

Tested only on the latest Mvvm Community Toolkit preview, you might be able to use source generators with tbc. This is confgured by adding a `SourceGeneratorReferences` array to the
Tested only on the latest Mvvm Community Toolkit preview, you might be able to use source generators with tbc. This is configured by adding a `SourceGeneratorReferences` array to the
`AssemblyCompiler` configuration element. Here you can include references to dlls, nuget packages or csproj files.

```
Expand Down Expand Up @@ -151,6 +151,33 @@ Tested only on the latest Mvvm Community Toolkit preview, you might be able to u
* For a `NuGetPackageReference` reference, tbc will scan the local nuget package cache for the provided package/version folder and to try to find assemblies that might contain generators, then pass them to the `AssemblyPath` method
* For a `Csproj` reference, tbc will parse the provided csproj file for nuget package references, then pass them to the `NuGetPackageReference` method.

## global usings

You can configure global usings by adding a `GlobalUsingsSources` array to the`AssemblyCompiler` configuration element.
Here you can include 'string lists' and/or search paths.

```
"GlobalUsingsSources": [
{
"Kind": "Text",
"Reference": "My.Namespace.A;My.Namespace.B"
},
{
"Kind": "SearchPath",
"Reference": "/Users/rdavis/Source/MyAppWithGlobalUsings/App1/App1/obj/Debug/",
"Context": "LastModified"
},
]
```

* For a `Text` source, tbc will split on ';' and add all the entries as usings
* For a `SearchPath` reference, tbc will scan the provided search path for `*.GlobalUsings.g.cs`.
If `Context` is "`LastModified`" or not specified, tbc will pick the most recently updated file of the files found. If `Context` is "`Merge`",
tbc will merge the contents of the files found.

# alpha quality

I've only used this for myself but on several production-complexity-level apps. I've only used it heavily on iOS. At least the sample works on Android too.
Expand Down
@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Tbc.Host.Components.Abstractions;
using Tbc.Host.Components.GlobalUsingsResolver.Models;

namespace Tbc.Host.Components.GlobalUsingsResolver;

public class GlobalUsingsResolver : ComponentBase<GlobalUsingsResolver>, IGlobalUsingsResolver
{
private IFileSystem _fileSystem;

public GlobalUsingsResolver(ILogger<GlobalUsingsResolver> logger, IFileSystem fileSystem) : base(logger)
{
_fileSystem = fileSystem;
}

public async Task<ResolveGlobalUsingsResponse> ResolveGlobalUsings(ResolveGlobalUsingsRequest request, CancellationToken canceller = default)
{
var sources = request.Sources;
var usings = ImmutableList.Create<string>();
var diagnostics = ImmutableDictionary.Create<string, object>();

foreach (var source in sources)
{
var (newUsings, newDiagnostics) =
source.Kind switch
{
GlobalUsingsSourceKind.Text => GetUsingsFromText(source.Reference),
GlobalUsingsSourceKind.SearchPath => GetUsingsFromSearchPath(source.Reference, source.Context),
_ => throw new ArgumentOutOfRangeException()
};

usings = usings.AddRange(newUsings);
diagnostics = diagnostics.AddRange(newDiagnostics);
}

usings = usings.Distinct().ToImmutableList();

return new ResolveGlobalUsingsResponse(
sources, usings.Distinct().ToImmutableList(),
usings.Any() ? String.Join(Environment.NewLine, usings) : null,
diagnostics);
}

public (List<string> Usings, Dictionary<string, object> Diagnostics) GetUsingsFromText(string text)
{
try
{
var usings = text.Split(';').Select(x => $"global using global::{x}").ToList();

return new(usings, new() { [text] = $"{usings.Count} usings extracted" });
}
catch (Exception ex) { return (new(), new() { [text] = ex }); }
}

public (List<string> Usings, Dictionary<string, object> Diagnostics) GetUsingsFromSearchPath(
string path, string maybeResolutionMethod)
{
maybeResolutionMethod ??= KnownGlobalUsingSearchPathResolutionApproach.LastModified;

var matches = _fileSystem.Directory.GetFiles(path, "*.GlobalUsings.g.cs", SearchOption.AllDirectories)
.OrderByDescending(x => _fileSystem.FileInfo.FromFileName(x).LastWriteTime)
.ToList();

var usings = matches
.Take(maybeResolutionMethod == KnownGlobalUsingSearchPathResolutionApproach.LastModified ? 1 : Int32.MaxValue)
.Select(f => (f, u: _fileSystem.File.ReadAllLines(f).Where(x => x.StartsWith("global using ")).ToList()))
.ToList();

return (usings.SelectMany(x => x.u).ToList(), usings.ToDictionary(x => x.f, x => (object) $"{x.u.Count} usings extracted"));
}
}
@@ -0,0 +1,10 @@
using System.Threading;
using System.Threading.Tasks;
using Tbc.Host.Components.GlobalUsingsResolver.Models;

namespace Tbc.Host.Components.GlobalUsingsResolver;

public interface IGlobalUsingsResolver
{
Task<ResolveGlobalUsingsResponse> ResolveGlobalUsings(ResolveGlobalUsingsRequest request, CancellationToken canceller = default);
}
@@ -0,0 +1,3 @@
namespace Tbc.Host.Components.GlobalUsingsResolver.Models;

public record GlobalUsingsSource(GlobalUsingsSourceKind Kind, string Reference, string? Context = null);
@@ -0,0 +1,7 @@
namespace Tbc.Host.Components.GlobalUsingsResolver.Models;

public enum GlobalUsingsSourceKind
{
Text,
SearchPath,
}
@@ -0,0 +1,7 @@
namespace Tbc.Host.Components.GlobalUsingsResolver.Models;

public static class KnownGlobalUsingSearchPathResolutionApproach
{
public const string LastModified = nameof(LastModified);
public const string MergeAll = nameof(MergeAll);
}
@@ -0,0 +1,5 @@
using System.Collections.Generic;

namespace Tbc.Host.Components.GlobalUsingsResolver.Models;

public record ResolveGlobalUsingsRequest(List<GlobalUsingsSource> Sources);
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Collections.Immutable;

namespace Tbc.Host.Components.GlobalUsingsResolver.Models;

public record ResolveGlobalUsingsResponse
(
List<GlobalUsingsSource> Sources,
ImmutableList<string> Usings,
string? UsingsSource,
ImmutableDictionary<string, object> Diagnostics
);
Expand Up @@ -17,6 +17,8 @@
using Tbc.Host.Components.CommandProcessor.Models;
using Tbc.Host.Components.FileEnvironment.Models;
using Tbc.Host.Components.FileWatcher.Models;
using Tbc.Host.Components.GlobalUsingsResolver;
using Tbc.Host.Components.GlobalUsingsResolver.Models;
using Tbc.Host.Components.IncrementalCompiler.Models;
using Tbc.Host.Components.SourceGeneratorResolver;
using Tbc.Host.Components.SourceGeneratorResolver.Models;
Expand All @@ -34,6 +36,7 @@ public class IncrementalCompiler : ComponentBase<IncrementalCompiler>, IIncremen

private readonly IFileSystem _fileSystem;
private readonly ISourceGeneratorResolver _sourceGeneratorResolver;
private readonly IGlobalUsingsResolver _globalUsingsResolver;

private int _incrementalCount = 0;
private readonly Guid _sessionGuid = Guid.NewGuid();
Expand All @@ -46,23 +49,28 @@ public class IncrementalCompiler : ComponentBase<IncrementalCompiler>, IIncremen
public ImmutableDictionary<SourceGeneratorReference,ResolveSourceGeneratorsResponse> SourceGeneratorResolution { get; set; }
public (IEnumerable<ISourceGenerator> Srcs, IEnumerable<IIncrementalGenerator> Incs) SourceGenerators { get; set; }
public bool AnySourceGenerators => SourceGenerators.Srcs.Any() || SourceGenerators.Incs.Any();

public ResolveGlobalUsingsResponse GlobalUsingResolution { get; set; }

public CSharpCompilation CurrentCompilation { get; set; }
public Dictionary<string, SyntaxTree> RawTrees { get; } = new();


public List<string> StagedFiles
=> CurrentCompilation.SyntaxTrees.Select(x => x.FilePath).ToList();

public IncrementalCompiler(
string clientIdentifier,
AssemblyCompilationOptions options,
IFileSystem fileSystem, ILogger<IncrementalCompiler> logger,
ISourceGeneratorResolver sourceGeneratorResolver
ISourceGeneratorResolver sourceGeneratorResolver,
IGlobalUsingsResolver globalUsingsResolver
) : base(logger)
{
_options = options;
_fileSystem = fileSystem;
_sourceGeneratorResolver = sourceGeneratorResolver;
_globalUsingsResolver = globalUsingsResolver;
_identifier = clientIdentifier;

var cscOptions = new CSharpCompilationOptions(
Expand All @@ -73,7 +81,7 @@ ISourceGeneratorResolver sourceGeneratorResolver
SourceGeneratorResolution = ResolveSourceGenerators();
SourceGenerators =
(SourceGeneratorResolution.SelectMany(x => x.Value.SourceGenerators).DistinctBySelector(x => x.GetType()).ToList(),
SourceGeneratorResolution.SelectMany(x => x.Value.IncrementalGenerators).DistinctBySelector(x => x.GetType()).ToList());
SourceGeneratorResolution.SelectMany(x => x.Value.IncrementalGenerators).DistinctBySelector(x => x.GetType()).ToList());

Logger.LogInformation("Source Generator Resolution: {Count} generator(s)", SourceGeneratorResolution);
foreach (var resolutionOutcome in SourceGeneratorResolution)
Expand All @@ -84,6 +92,20 @@ ISourceGeneratorResolver sourceGeneratorResolver

CurrentCompilation =
CSharpCompilation.Create("r2", options: cscOptions);

GlobalUsingResolution =
_globalUsingsResolver.ResolveGlobalUsings(new ResolveGlobalUsingsRequest(options.GlobalUsingsSources)).Result;

if (GlobalUsingResolution.Usings.Any() && GlobalUsingResolution.UsingsSource is { } gus)
CurrentCompilation = CurrentCompilation.AddSyntaxTrees(
CSharpSyntaxTree.ParseText(
gus,
CSharpParseOptions.Default
.WithLanguageVersion(LanguageVersion.Preview)
.WithKind(SourceCodeKind.Regular)
.WithPreprocessorSymbols(_options.PreprocessorSymbols.ToArray()),
path: "",
Encoding.Default));
}

private ImmutableDictionary<SourceGeneratorReference, ResolveSourceGeneratorsResponse> ResolveSourceGenerators()
Expand Down
3 changes: 3 additions & 0 deletions src/components/tbc.host/Config/AssemblyCompilationOptions.cs
@@ -1,4 +1,6 @@
using System.Collections.Generic;
using Tbc.Host.Components.GlobalUsingsResolver;
using Tbc.Host.Components.GlobalUsingsResolver.Models;
using Tbc.Host.Components.SourceGeneratorResolver;
using Tbc.Host.Components.SourceGeneratorResolver.Models;

Expand All @@ -13,5 +15,6 @@ public class AssemblyCompilationOptions
= new List<string>();
public string WriteAssembliesPath { get; set; }
public List<SourceGeneratorReference> SourceGeneratorReferences { get; set; } = new();
public List<GlobalUsingsSource> GlobalUsingsSources { get; set; } = new();
}
}

0 comments on commit 9f9167c

Please sign in to comment.