Skip to content
This repository has been archived by the owner on Dec 12, 2020. It is now read-only.

Commit

Permalink
feat: Enable C#8 NRTs (nullable enable) (#225)
Browse files Browse the repository at this point in the history
* Enable globally nullability

Except for the tool project, because of the command line sources

* Validate arguments of public entrypoints

* Update style cop analyzer to get rid of the false positive warning

* Adapt nullability according documentation

* Respect possible null reference

Co-authored-by: Manuel Pfemeter <m>
Co-authored-by: Amadeusz Sadowski <[email protected]>
  • Loading branch information
manne and amis92 authored Apr 15, 2020
1 parent f3a03fc commit d8cb83e
Show file tree
Hide file tree
Showing 18 changed files with 139 additions and 101 deletions.
68 changes: 44 additions & 24 deletions src/CodeGeneration.Roslyn.Engine/CompilationGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ namespace CodeGeneration.Roslyn.Engine
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Validation;

/// <summary>
/// Runs code generation for every applicable document and handles resulting syntax trees,
Expand All @@ -31,37 +30,58 @@ public class CompilationGenerator
private readonly List<string> generatedFiles = new List<string>();
private readonly List<string> additionalWrittenFiles = new List<string>();
private readonly List<string> loadedAssemblies = new List<string>();
private readonly Dictionary<string, (PluginLoader loader, Assembly assembly)> cachedPlugins = new Dictionary<string, (PluginLoader, Assembly)>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, (PluginLoader Loader, Assembly Assembly)> cachedPlugins = new Dictionary<string, (PluginLoader, Assembly)>(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Gets or sets the list of paths of files to be compiled.
/// Initializes a new instance of the <see cref="CompilationGenerator"/> class.
/// </summary>
public IReadOnlyList<string> Compile { get; set; }
/// <param name="projectDirectory">The directory with the project file.</param>
/// <param name="compile">The list of paths of files to be compiled.</param>
/// <param name="referencePath">The list of paths to reference assemblies.</param>
/// <param name="preprocessorSymbols">A set of preprocessor symbols to define.</param>
/// <param name="pluginPaths">The paths to plugins.</param>
/// <param name="intermediateOutputDirectory">The path to the directory that contains generated source files.</param>
/// <param name="buildProperties">The build properties to expose to generators.</param>
public CompilationGenerator(string? projectDirectory, IReadOnlyList<string> compile, IReadOnlyList<string> referencePath, IEnumerable<string> preprocessorSymbols, IReadOnlyList<string> pluginPaths, string intermediateOutputDirectory, IReadOnlyDictionary<string, string> buildProperties)
{
ProjectDirectory = projectDirectory;
Compile = compile ?? throw new ArgumentNullException(nameof(compile));
ReferencePath = referencePath ?? throw new ArgumentNullException(nameof(referencePath));
PreprocessorSymbols = preprocessorSymbols ?? throw new ArgumentNullException(nameof(preprocessorSymbols));
PluginPaths = pluginPaths ?? throw new ArgumentNullException(nameof(pluginPaths));
IntermediateOutputDirectory = intermediateOutputDirectory ?? throw new ArgumentNullException(nameof(intermediateOutputDirectory));
BuildProperties = buildProperties ?? throw new ArgumentNullException(nameof(buildProperties));
}

/// <summary>
/// Gets or sets the list of paths to reference assemblies.
/// Gets the list of paths of files to be compiled.
/// </summary>
public IReadOnlyList<string> ReferencePath { get; set; }
public IReadOnlyList<string> Compile { get; }

/// <summary>
/// Gets or sets a set of preprocessor symbols to define.
/// Gets the list of paths to reference assemblies.
/// </summary>
public IEnumerable<string> PreprocessorSymbols { get; set; }
public IReadOnlyList<string> ReferencePath { get; }

/// <summary>
/// Gets or sets the paths to plugins.
/// Gets a set of preprocessor symbols to define.
/// </summary>
public IReadOnlyList<string> PluginPaths { get; set; } = new List<string>();
public IEnumerable<string> PreprocessorSymbols { get; }

/// <summary>
/// Gets or sets the build properties to expose to generators.
/// Gets the build properties to expose to generators.
/// </summary>
public IReadOnlyDictionary<string, string> BuildProperties { get; set; }
public IReadOnlyDictionary<string, string> BuildProperties { get; }

/// <summary>
/// Gets or sets the path to the directory that contains generated source files.
/// Gets the paths to plugins.
/// </summary>
public string IntermediateOutputDirectory { get; set; }
public IReadOnlyList<string> PluginPaths { get; }

/// <summary>
/// Gets the path to the directory that contains generated source files.
/// </summary>
public string IntermediateOutputDirectory { get; }

/// <summary>
/// Gets the set of files generated after <see cref="GenerateAsync"/> is invoked.
Expand All @@ -79,22 +99,22 @@ public class CompilationGenerator
public IEnumerable<string> EmptyGeneratedFiles => this.emptyGeneratedFiles;

/// <summary>
/// Gets or sets the directory with the project file.
/// Gets the directory with the project file.
/// </summary>
public string ProjectDirectory { get; set; }
public string? ProjectDirectory { get; }

/// <summary>
/// Runs the code generation as configured using this instance's properties.
/// </summary>
/// <param name="progress">Optional handler of diagnostics provided by code generator.</param>
/// <param name="cancellationToken">Cancellation token to interrupt async operations.</param>
/// <returns>A <see cref="Task.CompletedTask"/>.</returns>
public async Task GenerateAsync(IProgress<Diagnostic> progress = null, CancellationToken cancellationToken = default)
public async Task GenerateAsync(IProgress<Diagnostic> progress, CancellationToken cancellationToken = default)
{
Verify.Operation(this.Compile != null, $"{nameof(Compile)} must be set first.");
Verify.Operation(this.ReferencePath != null, $"{nameof(ReferencePath)} must be set first.");
Verify.Operation(this.PluginPaths != null, $"{nameof(PluginPaths)} must be set first.");
Verify.Operation(this.IntermediateOutputDirectory != null, $"{nameof(IntermediateOutputDirectory)} must be set first.");
if (progress is null)
{
throw new ArgumentNullException(nameof(progress));
}

var compilation = this.CreateCompilation(cancellationToken);

Expand Down Expand Up @@ -178,12 +198,12 @@ public async Task GenerateAsync(IProgress<Diagnostic> progress = null, Cancellat
}
}

private Assembly LoadPlugin(AssemblyName assemblyName)
private Assembly? LoadPlugin(AssemblyName assemblyName)
{
if (cachedPlugins.TryGetValue(assemblyName.Name, out var cached))
{
Logger.Info($"CGR retrieved cached plugin for {assemblyName.Name}: {cached.assembly.Location}");
return cached.assembly;
Logger.Info($"CGR retrieved cached plugin for {assemblyName.Name}: {cached.Assembly.Location}");
return cached.Assembly;
}
Logger.Info($"CGR looking up plugin {assemblyName.Name}");
var pluginPath = PluginPaths.FirstOrDefault(IsRequestedPlugin);
Expand Down
73 changes: 34 additions & 39 deletions src/CodeGeneration.Roslyn.Engine/DocumentTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ public static class DocumentTransform
public static async Task<SyntaxTree> TransformAsync(
CSharpCompilation compilation,
SyntaxTree inputDocument,
string projectDirectory,
string? projectDirectory,
IReadOnlyDictionary<string, string> buildProperties,
Func<AssemblyName, Assembly> assemblyLoader,
Func<AssemblyName, Assembly?> assemblyLoader,
IProgress<Diagnostic> progress,
CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -135,11 +135,11 @@ private static ImmutableArray<AttributeData> GetAttributeData(Compilation compil
}
}

private static IEnumerable<ICodeGenerator> FindCodeGenerators(ImmutableArray<AttributeData> nodeAttributes, Func<AssemblyName, Assembly> assemblyLoader)
private static IEnumerable<ICodeGenerator> FindCodeGenerators(ImmutableArray<AttributeData> nodeAttributes, Func<AssemblyName, Assembly?> assemblyLoader)
{
foreach (var attributeData in nodeAttributes)
{
Type generatorType = GetCodeGeneratorTypeForAttribute(attributeData.AttributeClass, assemblyLoader);
Type? generatorType = GetCodeGeneratorTypeForAttribute(attributeData.AttributeClass, assemblyLoader);
if (generatorType != null)
{
ICodeGenerator generator;
Expand All @@ -158,53 +158,48 @@ private static IEnumerable<ICodeGenerator> FindCodeGenerators(ImmutableArray<Att
}
}

private static Type GetCodeGeneratorTypeForAttribute(INamedTypeSymbol attributeType, Func<AssemblyName, Assembly> assemblyLoader)
private static Type? GetCodeGeneratorTypeForAttribute(INamedTypeSymbol attributeType, Func<AssemblyName, Assembly?> assemblyLoader)
{
Requires.NotNull(assemblyLoader, nameof(assemblyLoader));

if (attributeType != null)
foreach (var generatorCandidateAttribute in attributeType.GetAttributes())
{
foreach (var generatorCandidateAttribute in attributeType.GetAttributes())
if (generatorCandidateAttribute.AttributeClass.Name == typeof(CodeGenerationAttributeAttribute).Name)
{
if (generatorCandidateAttribute.AttributeClass.Name == typeof(CodeGenerationAttributeAttribute).Name)
string? assemblyName = null;
string? fullTypeName = null;
TypedConstant firstArg = generatorCandidateAttribute.ConstructorArguments.Single();
if (firstArg.Value is string typeName)
{
string assemblyName = null;
string fullTypeName = null;
TypedConstant firstArg = generatorCandidateAttribute.ConstructorArguments.Single();
if (firstArg.Value is string typeName)
// This string is the full name of the type, which MAY be assembly-qualified.
int commaIndex = typeName.IndexOf(',');
bool isAssemblyQualified = commaIndex >= 0;
if (isAssemblyQualified)
{
// This string is the full name of the type, which MAY be assembly-qualified.
int commaIndex = typeName.IndexOf(',');
bool isAssemblyQualified = commaIndex >= 0;
if (isAssemblyQualified)
{
fullTypeName = typeName.Substring(0, commaIndex);
assemblyName = typeName.Substring(commaIndex + 1).Trim();
}
else
{
fullTypeName = typeName;
assemblyName = generatorCandidateAttribute.AttributeClass.ContainingAssembly.Name;
}
fullTypeName = typeName.Substring(0, commaIndex);
assemblyName = typeName.Substring(commaIndex + 1).Trim();
}
else if (firstArg.Value is INamedTypeSymbol typeOfValue)
else
{
// This was a typeof(T) expression
fullTypeName = GetFullTypeName(typeOfValue);
assemblyName = typeOfValue.ContainingAssembly.Name;
fullTypeName = typeName;
assemblyName = generatorCandidateAttribute.AttributeClass.ContainingAssembly.Name;
}
}
else if (firstArg.Value is INamedTypeSymbol typeOfValue)
{
// This was a typeof(T) expression
fullTypeName = GetFullTypeName(typeOfValue);
assemblyName = typeOfValue.ContainingAssembly.Name;
}

if (assemblyName != null)
if (assemblyName != null)
{
var assembly = assemblyLoader(new AssemblyName(assemblyName));
if (assembly != null)
{
var assembly = assemblyLoader(new AssemblyName(assemblyName));
if (assembly != null)
{
return assembly.GetType(fullTypeName);
}
return assembly.GetType(fullTypeName);
}

Verify.FailOperation("Unable to find code generator: {0} in {1}", fullTypeName, assemblyName);
}

Verify.FailOperation("Unable to find code generator: {0} in {1}", fullTypeName, assemblyName);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(TransformationCon

IEnumerable<MemberDeclarationSyntax> GeneratePartialClass()
{
var classDeclaration = context.ProcessingNode as ClassDeclarationSyntax;
yield return classDeclaration
.AddMembers(CreateExampleBuildProperty());
if (context.ProcessingNode is ClassDeclarationSyntax classDeclaration)
{
yield return classDeclaration
.AddMembers(CreateExampleBuildProperty());
}
}

MemberDeclarationSyntax CreateExampleBuildProperty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class AddGeneratedAttributeGenerator : RichBaseGenerator
public AddGeneratedAttributeGenerator(AttributeData attributeData)
: base(attributeData)
{
Attribute = (string)AttributeData.ConstructorArguments[0].Value;
Attribute = (string)AttributeData.ConstructorArguments[0].Value!;
}

public string Attribute { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class AddGeneratedExternGenerator : RichBaseGenerator
public AddGeneratedExternGenerator(AttributeData attributeData)
: base(attributeData)
{
Extern = (string)AttributeData.ConstructorArguments[0].Value;
Extern = (string)AttributeData.ConstructorArguments[0].Value!;
}

public string Extern { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class AddGeneratedUsingGenerator : RichBaseGenerator
public AddGeneratedUsingGenerator(AttributeData attributeData)
: base(attributeData)
{
Using = (string)AttributeData.ConstructorArguments[0].Value;
Using = (string)AttributeData.ConstructorArguments[0].Value!;
}

public string Using { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class DuplicateInOtherNamespaceGenerator : RichBaseGenerator
public DuplicateInOtherNamespaceGenerator(AttributeData attributeData)
: base(attributeData)
{
Namespace = (string)AttributeData.ConstructorArguments[0].Value;
Namespace = (string)AttributeData.ConstructorArguments[0].Value!;
}

public string Namespace { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public DuplicateWithSuffixGenerator(AttributeData attributeData)
{
Requires.NotNull(attributeData, nameof(attributeData));

this.suffix = (string)attributeData.ConstructorArguments[0].Value;
this.suffix = (string)attributeData.ConstructorArguments[0].Value!;
this.attributeData = attributeData;
this.data = this.attributeData.NamedArguments.ToImmutableDictionary(kv => kv.Key, kv => kv.Value);
}
Expand All @@ -34,7 +34,7 @@ public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(TransformationCon
{
var results = SyntaxFactory.List<MemberDeclarationSyntax>();

MemberDeclarationSyntax copy = null;
MemberDeclarationSyntax? copy = null;
var applyToClass = context.ProcessingNode as ClassDeclarationSyntax;
if (applyToClass != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public ExternalDuplicateWithSuffixGenerator(AttributeData attributeData)
{
Requires.NotNull(attributeData, nameof(attributeData));

this.suffix = (string)attributeData.ConstructorArguments[0].Value;
this.suffix = (string)attributeData.ConstructorArguments[0].Value!;
this.attributeData = attributeData;
this.data = this.attributeData.NamedArguments.ToImmutableDictionary(kv => kv.Key, kv => kv.Value);
}
Expand All @@ -35,7 +35,7 @@ public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(TransformationCon
{
var results = SyntaxFactory.List<MemberDeclarationSyntax>();

MemberDeclarationSyntax copy = null;
MemberDeclarationSyntax? copy = null;
var applyToClass = context.ProcessingNode as MethodDeclarationSyntax;
if (applyToClass != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(TransformationCon
{
var results = SyntaxFactory.List<MemberDeclarationSyntax>();

MemberDeclarationSyntax copy = null;
MemberDeclarationSyntax? copy = null;
var applyToClass = context.ProcessingNode as ClassDeclarationSyntax;
if (applyToClass != null)
{
Expand All @@ -37,7 +37,7 @@ public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(TransformationCon
var propertySymbol = context.SemanticModel.GetDeclaredSymbol(x);
var attribute = propertySymbol?.GetAttributes()
.FirstOrDefault(a => a.AttributeClass.Name == nameof(TestAttribute));
string suffix = "Suff" + string.Concat(attribute?.NamedArguments.Select(a => a.Value.Value.ToString()) ?? Enumerable.Empty<string>());
string suffix = "Suff" + string.Concat(attribute?.NamedArguments.Select(a => a.Value.Value!.ToString()) ?? Enumerable.Empty<string>());
return (MemberDeclarationSyntax)MethodDeclaration(ParseTypeName("void"), x.Identifier.ValueText + suffix)
.AddModifiers(Token(SyntaxKind.PublicKeyword))
.AddBodyStatements(Block());
Expand Down
2 changes: 1 addition & 1 deletion src/CodeGeneration.Roslyn.Tests/CodeGenerationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public class Foo
public partial class MultipliedBar
{
[Test(X = 10, Y = 20)]
public string Value { get; set; }
public string Value { get; set; } = "";
}

[AddExampleBuildProperty]
Expand Down
Loading

0 comments on commit d8cb83e

Please sign in to comment.