Skip to content

Commit f126925

Browse files
committed
RoslynCompiler: reorganizing the way we add namespaces/assemblies automatically
1 parent adf0766 commit f126925

File tree

2 files changed

+91
-98
lines changed

2 files changed

+91
-98
lines changed

src/Tools/TemplateBuilder/RoslynCompiler.cs

+89-96
Original file line numberDiff line numberDiff line change
@@ -16,64 +16,83 @@ namespace CodegenCS.TemplateBuilder
1616
internal class RoslynCompiler
1717
{
1818
protected readonly HashSet<PortableExecutableReference> _references = new HashSet<PortableExecutableReference>();
19-
protected readonly HashSet<string> _namespaces = new HashSet<string>();
19+
protected readonly Dictionary<string, Func<string, bool>> _namespaces = new Dictionary<string, Func<string, bool>>();
2020
protected readonly CSharpCompilationOptions _compilationOptions;
2121
protected readonly CSharpParseOptions _parseOptions;
2222
protected readonly string _dotNetCoreDir;
23+
protected readonly bool _verboseMode;
2324
protected ILogger _logger;
2425
protected TemplateBuilder _builder;
2526

26-
public RoslynCompiler(TemplateBuilder builder, ILogger logger, List<string> extraReferences, List<string> extraNamespaces)
27+
public RoslynCompiler(TemplateBuilder builder, ILogger logger, bool verboseMode)
2728
{
2829
_builder = builder;
2930
_logger = logger;
3031
var privateCoreLib = typeof(object).GetTypeInfo().Assembly.Location;
3132
_dotNetCoreDir = Path.GetDirectoryName(privateCoreLib);
3233

34+
_compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
35+
.WithOverflowChecks(true)
36+
.WithOptimizationLevel(OptimizationLevel.Release)
37+
//.WithUsings(_namespaces) // TODO: review why adding namespaces here doesn't make any difference - only AddMissingUsing (applied directly to tree) matters
38+
.WithWarningLevel(0);
39+
40+
// For Microsoft.CodeAnalysis.CSharp 4.2.2 LanguageVersion.Preview means C# 11 preview (which includes raw string literals)
41+
_parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview);
42+
_verboseMode = verboseMode;
43+
}
44+
45+
public void AddReferences(List<string> extraReferences, List<string> extraNamespaces)
46+
{
47+
// Some namespaces are always added (low risk of conflicting with user classes)
48+
// but some others will only be added if we detect (using Func) that they are required, to avoid type names conflict
49+
// Most regex below are checking for non-fully-qualified typename (no leading dot), because if you're using fully-qualified types you don't need "using"
50+
3351
#region Default Assemblies and Namespaces
3452

3553
#region Core (System, System.Text, System.Threading.Tasks)
36-
_namespaces.Add("System");
37-
_namespaces.Add("System.Text");
38-
_namespaces.Add("System.Threading");
39-
_namespaces.Add("System.Threading.Tasks");
54+
_namespaces.Add("System", null);
55+
_namespaces.Add("System.Text", null);
56+
_namespaces.Add("System.Threading", templateSource => Regex.IsMatch(templateSource, @"(?<!\.)\bTask\b")); //TODO: always add?
57+
_namespaces.Add("System.Threading.Tasks", templateSource => Regex.IsMatch(templateSource, @"(?<!\.)\bTask\b")); //TODO: always add?
4058

4159
// System.Private.CoreLib: this has all of the main core types in the runtime,
4260
// including most of the types that are on the System namespace (like mscorlib used to be on .net full framework), including object, List<>, Action<>, etc.
4361
AddAssembly(MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location)); // AddAssembly("System.Private.CoreLib.dll");
4462

4563
AddAssembly("netstandard.dll");
4664

65+
_namespaces.Add("System.Runtime.CompilerServices", null);
4766
AddAssembly("System.Runtime.dll");
4867
AddAssembly("System.Threading.dll");
4968
#endregion
5069

5170
#region System.Linq
52-
_namespaces.Add("System.Linq");
71+
_namespaces.Add("System.Linq", null);
5372
AddAssembly("System.Linq.dll");
5473
AddAssembly("System.Linq.Expressions.dll");
5574
AddAssembly("System.Core.dll");
5675
#endregion
5776

5877
#region System.Collections
59-
_namespaces.Add("System.Collections");
60-
_namespaces.Add("System.Collections.Generic");
61-
_namespaces.Add("System.Collections.Concurrent");
78+
_namespaces.Add("System.Collections", null);
79+
_namespaces.Add("System.Collections.Generic", null);
80+
_namespaces.Add("System.Collections.Concurrent", null);
6281
AddAssembly("System.Collections.dll"); //AddAssembly(MetadataReference.CreateFromFile(Assembly.Load("System.Collections").Location));
6382
AddAssembly("System.Collections.Concurrent.dll");
6483
AddAssembly("System.Collections.NonGeneric.dll");
6584
#endregion
6685

6786
#region System.Net.Http
68-
_namespaces.Add("System.Net");
69-
_namespaces.Add("System.Net.Http");
87+
_namespaces.Add("System.Net", null);
88+
_namespaces.Add("System.Net.Http", null);
7089
AddAssembly("System.Net.Http.dll");
7190
AddAssembly("System.Net.Primitives.dll");
7291
AddAssembly("System.Private.Uri.dll");
7392
#endregion
7493

7594
#region System.IO, System.Console
76-
_namespaces.Add("System.IO");
95+
_namespaces.Add("System.IO", null);
7796
AddAssembly("System.IO.dll");
7897
AddAssembly(MetadataReference.CreateFromFile(typeof(FileInfo).GetTypeInfo().Assembly.Location)); // System.IO.FileSystem
7998

@@ -82,15 +101,19 @@ public RoslynCompiler(TemplateBuilder builder, ILogger logger, List<string> extr
82101

83102
// InterpolatedColorConsole
84103
AddAssembly(MetadataReference.CreateFromFile(typeof(InterpolatedColorConsole.ColoredConsole).GetTypeInfo().Assembly.Location));
104+
_namespaces.Add("InterpolatedColorConsole.Symbols", templateSource =>
105+
Regex.IsMatch(templateSource, @"(?<!\.)\bPREVIOUS_COLOR\b") ||
106+
Regex.IsMatch(templateSource, @"(?<!\.)\bPREVIOUS_BACKGROUND_COLOR\b"));
107+
85108
#endregion
86109

87110

88111
AddAssembly("System.Reflection.dll");
89-
_namespaces.Add("System.Reflection");
112+
_namespaces.Add("System.Reflection", null);
90113

91114
AddAssembly(typeof(System.Text.RegularExpressions.Regex)); // .net framework
92115
AddAssembly("System.Text.RegularExpressions.dll");
93-
_namespaces.Add("System.Text.RegularExpressions");
116+
_namespaces.Add("System.Text.RegularExpressions", null);
94117

95118
#region TODO: CSharp / Roslyn Analyzers? To allow code generators to be based on Roslyn CodeAnalysis?
96119
//AddAssembly(typeof(Microsoft.CSharp.RuntimeBinder.RuntimeBinderException)); //AddAssembly("Microsoft.CSharp.dll");
@@ -99,55 +122,66 @@ public RoslynCompiler(TemplateBuilder builder, ILogger logger, List<string> extr
99122
// add nuget references
100123
#endregion
101124

102-
103-
104125
AddAssembly("System.ComponentModel.Primitives.dll");
105126

106-
107127
// CodegenCS / CodegenCS.Runtime / CodegenCS.Models.DbSchema
108128
AddAssembly(MetadataReference.CreateFromFile(typeof(CodegenCS.CodegenContext).GetTypeInfo().Assembly.Location));
109129
AddAssembly(MetadataReference.CreateFromFile(typeof(CodegenCS.Runtime.ExecutionContext).GetTypeInfo().Assembly.Location));
110130
AddAssembly(MetadataReference.CreateFromFile(typeof(CodegenCS.Models.IInputModel).GetTypeInfo().Assembly.Location));
111131
AddAssembly(MetadataReference.CreateFromFile(typeof(CodegenCS.DotNet.DotNetCodegenContext).GetTypeInfo().Assembly.Location));
112132
AddAssembly(MetadataReference.CreateFromFile(typeof(CodegenCS.Models.DbSchema.DatabaseSchema).GetTypeInfo().Assembly.Location));
113-
_namespaces.Add("CodegenCS");
114-
_namespaces.Add("CodegenCS.Runtime");
115-
_namespaces.Add("CodegenCS.Models");
116-
_namespaces.Add("CodegenCS.DotNet");
117-
_namespaces.Add("CodegenCS.Models.DbSchema");
133+
_namespaces.Add("CodegenCS", null);
134+
135+
_namespaces.Add("CodegenCS.Runtime", templateSource =>
136+
Regex.IsMatch(templateSource, @"(?<!\.)\bCommandLineArgs\b") ||
137+
Regex.IsMatch(templateSource, @"(?<!\.)\bIAutoBindCommandLineArgs\b") ||
138+
Regex.IsMatch(templateSource, @"(?<!\.)\bVSExecutionContext\b") ||
139+
Regex.IsMatch(templateSource, @"(?<!\.)\bExecutionContext\b") ||
140+
(Regex.IsMatch(templateSource, @"(?<!\.)\bILogger\b") && Regex.IsMatch(templateSource, @"\bWriteLine(\w*)Async\b")));
141+
_namespaces.Add("CodegenCS.Models", templateSource =>
142+
Regex.IsMatch(templateSource, @"(?<!\.)\bIInputModel\b") ||
143+
Regex.IsMatch(templateSource, @"(?<!\.)\bIJsonInputModel\b") ||
144+
Regex.IsMatch(templateSource, @"(?<!\.)\bIValidatableJsonInputModel\b") ||
145+
Regex.IsMatch(templateSource, @"(?<!\.)\bIModelFactory\b"));
146+
_namespaces.Add("CodegenCS.DotNet", null);
147+
_namespaces.Add("CodegenCS.Models.DbSchema", templateSource => Regex.IsMatch(templateSource, @"(?<!\.)\bDatabaseSchema\b"));
148+
_namespaces.Add("CodegenCS.Symbols", templateSource =>
149+
Regex.IsMatch(templateSource, @"(?<!\.)\bIF\(\b") ||
150+
Regex.IsMatch(templateSource, @"(?<!\.)\bIIF\(\b") ||
151+
Regex.IsMatch(templateSource, @"(?<!\.)\bBREAKIF\(\b") ||
152+
Regex.IsMatch(templateSource, @"(?<!\.)\bTLW\(\b") ||
153+
Regex.IsMatch(templateSource, @"(?<!\.)\bTTW\(\b") ||
154+
Regex.IsMatch(templateSource, @"(?<!\.)\bCOMMENT\(\b") ||
155+
Regex.IsMatch(templateSource, @"(?<!\.)\bRAW\(\b"));
118156

119157
AddAssembly(MetadataReference.CreateFromFile(typeof(NSwag.OpenApiDocument).GetTypeInfo().Assembly.Location)); // NSwag.Core
120158
AddAssembly(MetadataReference.CreateFromFile(typeof(NSwag.OpenApiYamlDocument).GetTypeInfo().Assembly.Location)); // NSwag.Core.Yaml
121159
AddAssembly(MetadataReference.CreateFromFile(typeof(NJsonSchema.JsonSchema).GetTypeInfo().Assembly.Location)); // NJsonSchema
122160
AddAssembly(MetadataReference.CreateFromFile(typeof(NJsonSchema.Annotations.JsonSchemaAttribute).GetTypeInfo().Assembly.Location)); // NJsonSchema.Annotations
161+
_namespaces.Add("NSwag", templateSource => Regex.IsMatch(templateSource, @"(?<!\.)\bOpenApiDocument\b"));
123162

124163
// Newtonsoft
125-
_namespaces.Add("Newtonsoft.Json");
164+
_namespaces.Add("Newtonsoft.Json", null); // maybe we should only add namespace if we find references like "JsonConvert"?
126165
AddAssembly(MetadataReference.CreateFromFile(typeof(Newtonsoft.Json.JsonConvert).GetTypeInfo().Assembly.Location));
127166

128167
AddAssembly(MetadataReference.CreateFromFile(typeof(System.CommandLine.Argument).GetTypeInfo().Assembly.Location));
129168
AddAssembly(MetadataReference.CreateFromFile(typeof(System.CommandLine.Binding.BindingContext).GetTypeInfo().Assembly.Location));
169+
// System.CommandLine.Command, System.CommandLine.ParseResult
170+
_namespaces.Add("System.CommandLine", templateSource =>
171+
Regex.IsMatch(templateSource, @"(?<!\.)\bConfigureCommand\b") ||
172+
Regex.IsMatch(templateSource, @"(?<!\.)\bParseResult\b"));
173+
_namespaces.Add("System.CommandLine.Binding", templateSource => Regex.IsMatch(templateSource, @"(?<!\.)\bBindingContext\b"));
174+
_namespaces.Add("System.CommandLine.Invocation", templateSource => Regex.IsMatch(templateSource, @"(?<!\.)\bInvocationContext\b"));
130175

131176
#endregion
132177

133-
// Add this library? //AddAssembly(typeof(RoslynCompiler));
178+
// Add this library (TemplateBuilder)? //AddAssembly(typeof(RoslynCompiler));
134179
// maybe just add AppDomain.CurrentDomain.GetAssemblies() ?
135180

136181
if (extraNamespaces != null)
137-
extraNamespaces.ForEach(ns => _namespaces.Add(ns));
182+
extraNamespaces.ForEach(ns => _namespaces.Add(ns, null));
138183
if (extraReferences != null)
139184
extraReferences.ForEach(rfc => AddAssembly(MetadataReference.CreateFromFile(rfc)));
140-
141-
_namespaces.Clear(); // TODO: review why these namespaces don't make any difference here - only AddMissingUsing (applied directly to tree) matters
142-
143-
_compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
144-
.WithOverflowChecks(true)
145-
.WithOptimizationLevel(OptimizationLevel.Release)
146-
.WithUsings(_namespaces)
147-
.WithWarningLevel(0);
148-
149-
// For Microsoft.CodeAnalysis.CSharp 4.2.2 LanguageVersion.Preview means C# 11 preview (which includes raw string literals)
150-
_parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview);
151185
}
152186

153187

@@ -219,8 +253,7 @@ public async Task<CompileResult> CompileAsync(string[] sources, string targetFil
219253
// ParseText really better? https://stackoverflow.com/questions/16338131/using-roslyn-to-parse-transform-generate-code-am-i-aiming-too-high-or-too-low
220254
//SyntaxFactory.ParseSyntaxTree(SourceText.From(text, Encoding.UTF8), options, filename);
221255

222-
AddMissingUsings(syntaxTrees);
223-
256+
await AddMissingUsings(syntaxTrees);
224257

225258
//TODO: support for top-level statements?
226259
CSharpCompilation compilation = CSharpCompilation.Create("assemblyName", syntaxTrees,
@@ -291,76 +324,36 @@ public async Task<CompileResult> CompileAsync(string[] sources, string targetFil
291324
}
292325

293326
}
294-
void AddMissingUsings(List<SyntaxTree> trees)
327+
async Task AddMissingUsings(List<SyntaxTree> trees)
295328
{
296329
for (int i = 0; i < trees.Count; i++)
297330
{
298331
var rootNode = trees[i].GetRoot() as CompilationUnitSyntax;
299-
AddMissingUsing(ref rootNode, "CodegenCS");
300-
301332

302333
string templateSource = rootNode.ToString(); //TODO: strip strings from CompilationUnitSyntax - we are only interested in checking the template control logic
303334

304-
// These namespaces probably won't conflict with anything
305-
AddMissingUsing(ref rootNode, "System");
306-
AddMissingUsing(ref rootNode, "System.Collections.Generic");
307-
AddMissingUsing(ref rootNode, "System.Linq");
308-
AddMissingUsing(ref rootNode, "System.IO");
309-
AddMissingUsing(ref rootNode, "System.Runtime.CompilerServices");
310-
AddMissingUsing(ref rootNode, "System.Text.RegularExpressions");
311-
AddMissingUsing(ref rootNode, "Newtonsoft.Json"); // I doubt this might conflict with anything. Maybe should search for JsonConvert and some other classes
312-
313-
// To avoid type names conflict we only add some usings if we detect as required
314-
// Most regex below are checking for non-fully-qualified typename (no leading dot).
315-
if (Regex.IsMatch(templateSource, @"(?<!\.)\bDatabaseSchema\b"))
316-
AddMissingUsing(ref rootNode, "CodegenCS.Models.DbSchema");
317-
if (Regex.IsMatch(templateSource, @"(?<!\.)\bOpenApiDocument\b"))
318-
AddMissingUsing(ref rootNode, "NSwag");
319-
if (Regex.IsMatch(templateSource, @"(?<!\.)\bCommandLineArgs\b")
320-
|| Regex.IsMatch(templateSource, @"(?<!\.)\bIAutoBindCommandLineArgs\b")
321-
|| Regex.IsMatch(templateSource, @"(?<!\.)\bVSExecutionContext\b")
322-
|| Regex.IsMatch(templateSource, @"(?<!\.)\bExecutionContext\b")
323-
)
324-
AddMissingUsing(ref rootNode, "CodegenCS.Runtime");
325-
if (Regex.IsMatch(templateSource, @"(?<!\.)\bILogger\b") && Regex.IsMatch(templateSource, @"\bWriteLine(\w*)Async\b"))
326-
AddMissingUsing(ref rootNode, "CodegenCS.Runtime");
327-
if (Regex.IsMatch(templateSource, @"(?<!\.)\bIInputModel\b")
328-
|| Regex.IsMatch(templateSource, @"(?<!\.)\bIJsonInputModel\b")
329-
|| Regex.IsMatch(templateSource, @"(?<!\.)\bIValidatableJsonInputModel\b")
330-
|| Regex.IsMatch(templateSource, @"(?<!\.)\bIModelFactory\b")
331-
)
332-
AddMissingUsing(ref rootNode, "CodegenCS.Models");
333-
334-
if (Regex.IsMatch(templateSource, @"(?<!\.)\bTask\b"))
335+
foreach (var ns in _namespaces)
335336
{
336-
AddMissingUsing(ref rootNode, "System.Threading");
337-
AddMissingUsing(ref rootNode, "System.Threading.Tasks");
337+
if (ns.Value == null)
338+
{
339+
if (_verboseMode)
340+
await _logger.WriteLineAsync(ConsoleColor.DarkGray, $"Automatically adding namespace \"{ns.Key}\"");
341+
AddMissingUsing(ref rootNode, ns.Key);
342+
}
343+
else if (ns.Value(templateSource))
344+
{
345+
if (_verboseMode)
346+
await _logger.WriteLineAsync(ConsoleColor.DarkGray, $"Automatically adding namespace \"{ns.Key}\" (due to matching regex)");
347+
AddMissingUsing(ref rootNode, ns.Key);
348+
}
338349
}
339350

340-
if (Regex.IsMatch(templateSource, @"(?<!\.)\bConfigureCommand\b") || Regex.IsMatch(templateSource, @"(?<!\.)\bParseResult\b"))
341-
AddMissingUsing(ref rootNode, "System.CommandLine"); // System.CommandLine.Command, System.CommandLine.ParseResult
342-
if (Regex.IsMatch(templateSource, @"(?<!\.)\bBindingContext\b"))
343-
AddMissingUsing(ref rootNode, "System.CommandLine.Binding");
344-
if (Regex.IsMatch(templateSource, @"(?<!\.)\bInvocationContext\b"))
345-
AddMissingUsing(ref rootNode, "System.CommandLine.Invocation");
346-
347-
if (Regex.IsMatch(templateSource, @"(?<!\.)\bIF\(\b") ||
348-
Regex.IsMatch(templateSource, @"(?<!\.)\bIIF\(\b") ||
349-
Regex.IsMatch(templateSource, @"(?<!\.)\bBREAKIF\(\b") ||
350-
Regex.IsMatch(templateSource, @"(?<!\.)\bTLW\(\b") ||
351-
Regex.IsMatch(templateSource, @"(?<!\.)\bTTW\(\b") ||
352-
Regex.IsMatch(templateSource, @"(?<!\.)\bCOMMENT\(\b") ||
353-
Regex.IsMatch(templateSource, @"(?<!\.)\bRAW\(\b"))
354-
AddMissingUsing(ref rootNode, "CodegenCS.Symbols", true);
355-
356-
if (Regex.IsMatch(templateSource, @"(?<!\.)\bPREVIOUS_COLOR\b") || Regex.IsMatch(templateSource, @"(?<!\.)\bPREVIOUS_BACKGROUND_COLOR\b"))
357-
AddMissingUsing(ref rootNode, "InterpolatedColorConsole.Symbols", true);
358-
359-
trees[i] = SyntaxFactory.SyntaxTree(rootNode, _parseOptions); // rootNode.SyntaxTree (without _parseOptions) would go back to C# 10 (and we need C# 11 preview)
351+
trees[i] = SyntaxFactory.SyntaxTree(rootNode, _parseOptions); // rootNode.SyntaxTree (without _parseOptions) would go back to C# 10 (and we need C# 11)
360352
}
361353
}
362-
void AddMissingUsing(ref CompilationUnitSyntax unit, string @namespace, bool isStatic = false)
354+
void AddMissingUsing(ref CompilationUnitSyntax unit, string @namespace)
363355
{
356+
bool isStatic = @namespace.Equals("CodegenCS.Symbols") || @namespace.Equals("InterpolatedColorConsole.Symbols"); //TODO: yeah, I know...
364357
var qualifiedName = SyntaxFactory.ParseName(@namespace);
365358
if (!unit.Usings.Select(d => d.Name.ToString()).Any(u => u == qualifiedName.ToString()))
366359
{

0 commit comments

Comments
 (0)