mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-06-09 10:06:34 +02:00
[Dependency] source generator (#6549)
* [Dependency] source generator No more reflection, no more codegen at runtime Also various changes to Roslyn helpers to make this easier to write. Requires all types with dependencies to be partial and not have readonly dependency fields. An analyzer enforces this at warning level, the previous injection strategies have remained in the code *for now* as a fallback. No fallback is available for [field: Dependency] properties, due to a Roslyn bug. Code Fixes exist. We love Roslyn * Release notes * Handle nullable dependencies These are bad but gotta deal with it. * Apply suggestions from code review Co-authored-by: Moony <moony@hellomouse.net> * Fine, let's not use collection expressions --------- Co-authored-by: Moony <moony@hellomouse.net>
This commit is contained in:
committed by
GitHub
parent
bbf199757c
commit
b4eb85ad3c
@@ -46,6 +46,8 @@ END TEMPLATE-->
|
||||
- `IParsable`, `ISpanParsable`, and `IUtf8SpanParsable` are now allowed by the sandbox.
|
||||
- Added `MarkupNode.IsPlainText` helper property.
|
||||
- Added an analyzer to detect and warn about `[Dependency]` fields with nullable types. These have never done anything special and are programming error.
|
||||
- Added a `[Dependency]` source generator. This should reduce runtime codegen overhead amount and reduce reflection use.
|
||||
- Existing code using dependency fields should be updated to be `partial` and not use `readonly`. Analyzers and code fixers exist for this. It is not yet an error, but will become one in the future.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.HasDependenciesAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
|
||||
[TestFixture]
|
||||
public sealed class HasDependenciesAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<HasDependenciesAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { code }
|
||||
},
|
||||
};
|
||||
|
||||
TestHelper.AddEmbeddedSources(test.TestState, "Robust.Shared.IoC.DependencyAttribute.cs");
|
||||
|
||||
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ReadOnlyFieldTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
public sealed partial class Foo
|
||||
{
|
||||
[Dependency] private readonly string _x;
|
||||
[Dependency] private string _y;
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(5,26): warning RA0049: Field '_x' is a [Dependency] but is readonly. This will be an error in the future.
|
||||
VerifyCS.Diagnostic(HasDependenciesAnalyzer.DiagnosticReadOnly).WithSpan(5, 26, 5, 34).WithArguments("_x")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NotPartialTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
public sealed class Foo
|
||||
{
|
||||
[Dependency] private string _y;
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(3,15): warning RA0048: Type 'Foo' has [Dependency] fields but is not partial. This will be required in the future.
|
||||
VerifyCS.Diagnostic(HasDependenciesAnalyzer.DiagnosticNotPartial).WithSpan(3, 15, 3, 20).WithArguments("Foo")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NotPartialNestedTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
public sealed class Foo
|
||||
{
|
||||
public sealed partial class Bar
|
||||
{
|
||||
[Dependency] private string _y;
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(5,27): warning RA0049: Type 'Foo.Bar' has [Dependency] fields but is nested in a non-partial type. This will be illegal in the future.
|
||||
VerifyCS.Diagnostic(HasDependenciesAnalyzer.DiagnosticNotPartialParent).WithSpan(5, 27, 5, 32).WithArguments("Foo.Bar")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NotPropertyField()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
public sealed partial class Bar
|
||||
{
|
||||
[field: Dependency] private string _y { get; } = "A";
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(5,5): warning RA0051: Property '_y' has a backing field marked with [Dependency]. This will be an error in the future.
|
||||
VerifyCS.Diagnostic(HasDependenciesAnalyzer.DiagnosticPropertyField).WithSpan(5, 5, 5, 58).WithArguments("_y")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
extern alias SerializationGenerator;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using NUnit.Framework;
|
||||
using Robust.Analyzers.Generators;
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(typeof(HasDependenciesGenerator))]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public sealed class HasDependenciesGeneratorTest
|
||||
{
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
var result = RunGenerator("""
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
public sealed partial class Foobar
|
||||
{
|
||||
[Dependency]
|
||||
public string Foo;
|
||||
}
|
||||
""");
|
||||
|
||||
ExpectNoDiagnostics(result);
|
||||
ExpectSource(
|
||||
result,
|
||||
"""
|
||||
// <auto-generated />
|
||||
|
||||
[global::Robust.Shared.IoC.HasDependenciesGeneratedAttribute]
|
||||
public partial class Foobar : global::Robust.Shared.IoC.IHasDependencies
|
||||
{
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
global::System.Type[] global::Robust.Shared.IoC.IHasDependencies.GetDependencyTypes()
|
||||
{
|
||||
return new global::System.Type[]
|
||||
{
|
||||
typeof(global::string)
|
||||
};
|
||||
}
|
||||
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
void global::Robust.Shared.IoC.IHasDependencies.Inject(global::System.ReadOnlySpan<object> instances)
|
||||
{
|
||||
Foo = (global::string)instances[0];
|
||||
}
|
||||
}
|
||||
|
||||
""");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInheritGeneric()
|
||||
{
|
||||
var result = RunGenerator("""
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
public partial class Foo<T>
|
||||
{
|
||||
[Dependency] string _x = null!;
|
||||
}
|
||||
|
||||
public sealed partial class Bar : Foo<int>
|
||||
{
|
||||
[Dependency]
|
||||
public string _heck = null!;
|
||||
}
|
||||
""");
|
||||
|
||||
ExpectNoDiagnostics(result);
|
||||
Assert.That(result.GeneratedSources, Has.Length.EqualTo(2));
|
||||
|
||||
ExpectNamedSource(
|
||||
result,
|
||||
"Foo`1.g.cs",
|
||||
"""
|
||||
// <auto-generated />
|
||||
|
||||
[global::Robust.Shared.IoC.HasDependenciesGeneratedAttribute]
|
||||
public partial class Foo<T> : global::Robust.Shared.IoC.IHasDependencies
|
||||
{
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
global::System.Type[] global::Robust.Shared.IoC.IHasDependencies.GetDependencyTypes()
|
||||
{
|
||||
return GetDependencyTypesImpl();
|
||||
}
|
||||
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
void global::Robust.Shared.IoC.IHasDependencies.Inject(global::System.ReadOnlySpan<object> instances)
|
||||
{
|
||||
InjectImpl(instances);
|
||||
}
|
||||
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
protected virtual global::System.Type[] GetDependencyTypesImpl()
|
||||
{
|
||||
return new global::System.Type[]
|
||||
{
|
||||
typeof(global::string)
|
||||
};
|
||||
}
|
||||
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
protected virtual void InjectImpl(global::System.ReadOnlySpan<object> instances)
|
||||
{
|
||||
_x = (global::string)instances[0];
|
||||
}
|
||||
}
|
||||
|
||||
""");
|
||||
|
||||
ExpectNamedSource(
|
||||
result,
|
||||
"Bar.g.cs",
|
||||
"""
|
||||
// <auto-generated />
|
||||
|
||||
[global::Robust.Shared.IoC.HasDependenciesGeneratedAttribute]
|
||||
public partial class Bar
|
||||
{
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
protected override global::System.Type[] GetDependencyTypesImpl()
|
||||
{
|
||||
var baseTypes = base.GetDependencyTypesImpl();
|
||||
var types = new global::System.Type[baseTypes.Length + 1];
|
||||
|
||||
types[0] = typeof(global::string);
|
||||
|
||||
global::System.Array.Copy(baseTypes, 0, types, 1, baseTypes.Length);
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
protected override void InjectImpl(global::System.ReadOnlySpan<object> instances)
|
||||
{
|
||||
_heck = (global::string)instances[0];
|
||||
|
||||
base.InjectImpl(instances.Slice(1));
|
||||
}
|
||||
}
|
||||
|
||||
""");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGenericInherit()
|
||||
{
|
||||
var result = RunGenerator("""
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
public partial class Foo
|
||||
{
|
||||
[Dependency] string _x = null!;
|
||||
}
|
||||
|
||||
public sealed partial class Bar<T> : Foo
|
||||
{
|
||||
[Dependency]
|
||||
public string _heck = null!;
|
||||
}
|
||||
""");
|
||||
|
||||
ExpectNoDiagnostics(result);
|
||||
Assert.That(result.GeneratedSources, Has.Length.EqualTo(2));
|
||||
|
||||
ExpectNamedSource(
|
||||
result,
|
||||
"Foo.g.cs",
|
||||
"""
|
||||
// <auto-generated />
|
||||
|
||||
[global::Robust.Shared.IoC.HasDependenciesGeneratedAttribute]
|
||||
public partial class Foo : global::Robust.Shared.IoC.IHasDependencies
|
||||
{
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
global::System.Type[] global::Robust.Shared.IoC.IHasDependencies.GetDependencyTypes()
|
||||
{
|
||||
return GetDependencyTypesImpl();
|
||||
}
|
||||
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
void global::Robust.Shared.IoC.IHasDependencies.Inject(global::System.ReadOnlySpan<object> instances)
|
||||
{
|
||||
InjectImpl(instances);
|
||||
}
|
||||
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
protected virtual global::System.Type[] GetDependencyTypesImpl()
|
||||
{
|
||||
return new global::System.Type[]
|
||||
{
|
||||
typeof(global::string)
|
||||
};
|
||||
}
|
||||
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
protected virtual void InjectImpl(global::System.ReadOnlySpan<object> instances)
|
||||
{
|
||||
_x = (global::string)instances[0];
|
||||
}
|
||||
}
|
||||
|
||||
""");
|
||||
|
||||
ExpectNamedSource(
|
||||
result,
|
||||
"Bar`1.g.cs",
|
||||
"""
|
||||
// <auto-generated />
|
||||
|
||||
[global::Robust.Shared.IoC.HasDependenciesGeneratedAttribute]
|
||||
public partial class Bar<T>
|
||||
{
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
protected override global::System.Type[] GetDependencyTypesImpl()
|
||||
{
|
||||
var baseTypes = base.GetDependencyTypesImpl();
|
||||
var types = new global::System.Type[baseTypes.Length + 1];
|
||||
|
||||
types[0] = typeof(global::string);
|
||||
|
||||
global::System.Array.Copy(baseTypes, 0, types, 1, baseTypes.Length);
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
protected override void InjectImpl(global::System.ReadOnlySpan<object> instances)
|
||||
{
|
||||
_heck = (global::string)instances[0];
|
||||
|
||||
base.InjectImpl(instances.Slice(1));
|
||||
}
|
||||
}
|
||||
|
||||
""");
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void TestReadOnly()
|
||||
{
|
||||
var result = RunGenerator("""
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
public sealed partial class Foobar
|
||||
{
|
||||
[Dependency]
|
||||
public readonly string Foo;
|
||||
}
|
||||
""");
|
||||
|
||||
ExpectNoDiagnostics(result);
|
||||
ExpectNoSource(result);
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void TestNotPartial()
|
||||
{
|
||||
var result = RunGenerator("""
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
public sealed class Foobar
|
||||
{
|
||||
[Dependency]
|
||||
public string Foo;
|
||||
}
|
||||
""");
|
||||
|
||||
ExpectNoDiagnostics(result);
|
||||
ExpectNoSource(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNested()
|
||||
{
|
||||
var result = RunGenerator("""
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
public sealed partial class Real
|
||||
{
|
||||
public sealed partial class Foobar
|
||||
{
|
||||
[Dependency]
|
||||
public string Foo;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
ExpectNoDiagnostics(result);
|
||||
ExpectSource(
|
||||
result,
|
||||
"""
|
||||
// <auto-generated />
|
||||
|
||||
public partial class Real
|
||||
{
|
||||
[global::Robust.Shared.IoC.HasDependenciesGeneratedAttribute]
|
||||
public partial class Foobar : global::Robust.Shared.IoC.IHasDependencies
|
||||
{
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
global::System.Type[] global::Robust.Shared.IoC.IHasDependencies.GetDependencyTypes()
|
||||
{
|
||||
return new global::System.Type[]
|
||||
{
|
||||
typeof(global::string)
|
||||
};
|
||||
}
|
||||
|
||||
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
|
||||
void global::Robust.Shared.IoC.IHasDependencies.Inject(global::System.ReadOnlySpan<object> instances)
|
||||
{
|
||||
Foo = (global::string)instances[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
""");
|
||||
}
|
||||
|
||||
private static void ExpectSource(GeneratorRunResult result, string expected)
|
||||
{
|
||||
Assert.That(result.GeneratedSources, Has.Length.EqualTo(1));
|
||||
|
||||
var source = result.GeneratedSources[0];
|
||||
|
||||
Assert.That(source.SourceText.ToString().ReplaceLineEndings(), Is.EqualTo(expected.ReplaceLineEndings()));
|
||||
}
|
||||
|
||||
private static void ExpectNamedSource(GeneratorRunResult result, string name, string expected)
|
||||
{
|
||||
var source = result.GeneratedSources.Single(s => s.HintName == name);
|
||||
|
||||
Assert.That(source.SourceText.ToString().ReplaceLineEndings(), Is.EqualTo(expected.ReplaceLineEndings()));
|
||||
}
|
||||
|
||||
private static void ExpectNoSource(GeneratorRunResult result)
|
||||
{
|
||||
Assert.That(result.GeneratedSources, Is.Empty);
|
||||
}
|
||||
|
||||
private static void ExpectNoDiagnostics(GeneratorRunResult result)
|
||||
{
|
||||
Assert.That(result.Diagnostics, Is.Empty);
|
||||
}
|
||||
|
||||
private static void ExpectDiagnostics(GeneratorRunResult result, (string code, LinePositionSpan span)[] diagnostics)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result.Diagnostics, Has.Length.EqualTo(diagnostics.Length));
|
||||
foreach (var (code, span) in diagnostics)
|
||||
{
|
||||
Assert.That(
|
||||
result.Diagnostics.Any(x => x.Id == code && x.Location.GetLineSpan().Span == span),
|
||||
$"Expected diagnostic with code {code} and location {span}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static GeneratorRunResult RunGenerator(string source)
|
||||
{
|
||||
var compilation = (Compilation)CSharpCompilation.Create("compilation",
|
||||
[
|
||||
CSharpSyntaxTree.ParseText(source, path: "Source.cs"),
|
||||
..TestHelper.GetEmbeddedSyntaxTrees(
|
||||
"Robust.Shared.IoC.DependencyAttribute.cs",
|
||||
"Robust.Shared.IoC.IHasDependencies.cs"),
|
||||
],
|
||||
new[] { MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location) },
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||
|
||||
var generator = new HasDependenciesGenerator();
|
||||
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
|
||||
driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out _);
|
||||
var result = driver.GetRunResult();
|
||||
|
||||
return result.Results[0];
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ObsoleteInheritanceAttribute.cs" LogicalName="Robust.Shared.Analyzers.ObsoleteInheritanceAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ValidateMemberAttribute.cs" LogicalName="Robust.Shared.Analyzers.ValidateMemberAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\IoC\DependencyAttribute.cs" LogicalName="Robust.Shared.IoC.DependencyAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\IoC\IHasDependencies.cs" LogicalName="Robust.Shared.IoC.IHasDependencies.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\GameObjects\EventBusAttributes.cs" LogicalName="Robust.Shared.GameObjects.EventBusAttributes.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Serialization\NetSerializableAttribute.cs" LogicalName="Robust.Shared.Serialization.NetSerializableAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Prototypes\Attributes.cs" LogicalName="Robust.Shared.Prototypes.Attributes.cs" LinkBase="Implementations" />
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
@@ -15,8 +19,18 @@ public static class TestHelper
|
||||
{
|
||||
foreach (var fileName in embeddedFiles)
|
||||
{
|
||||
using var stream = typeof(AccessAnalyzer_Test).Assembly.GetManifestResourceStream(fileName)!;
|
||||
state.Sources.Add((fileName, SourceText.From(stream)));
|
||||
state.Sources.Add((fileName, GetEmbeddedFile(fileName)));
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<SyntaxTree> GetEmbeddedSyntaxTrees(params string[] embeddedFiles)
|
||||
{
|
||||
return embeddedFiles.Select(fileName => CSharpSyntaxTree.ParseText(GetEmbeddedFile(fileName)));
|
||||
}
|
||||
|
||||
private static SourceText GetEmbeddedFile(string fileName)
|
||||
{
|
||||
using var stream = typeof(TestHelper).Assembly.GetManifestResourceStream(fileName)!;
|
||||
return SourceText.From(stream);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Robust.Roslyn.Shared;
|
||||
using Robust.Roslyn.Shared.Helpers;
|
||||
|
||||
namespace Robust.Analyzers.Generators;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
public sealed class HasDependenciesGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string DependencyAttributeName = "Robust.Shared.IoC.DependencyAttribute";
|
||||
private const string IHasDependenciesName = "Robust.Shared.IoC.IHasDependencies";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var fields = context.SyntaxProvider.ForAttributeWithMetadataName(
|
||||
DependencyAttributeName,
|
||||
static (node, _) => node is VariableDeclaratorSyntax,
|
||||
static (syntaxContext, token) =>
|
||||
{
|
||||
var field = (IFieldSymbol)syntaxContext.TargetSymbol;
|
||||
var fieldType = (INamedTypeSymbol)field.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated);
|
||||
var owningType = (INamedTypeSymbol)field.ContainingSymbol;
|
||||
|
||||
var declarationSyntax = (TypeDeclarationSyntax)owningType.DeclaringSyntaxReferences[0]
|
||||
.GetSyntax(token);
|
||||
|
||||
var partialTypeInfo = PartialTypeInfo.FromSymbol(owningType, declarationSyntax);
|
||||
|
||||
return (partialTypeInfo, FieldInfo: new FieldInfo(field.Name, fieldType.ToDisplayString(), field.IsReadOnly));
|
||||
});
|
||||
|
||||
var grouped = fields
|
||||
.Where(p => p.partialTypeInfo.IsValid)
|
||||
.Collect()
|
||||
.SelectMany(static (array, _) =>
|
||||
{
|
||||
return array.GroupBy(info => info.partialTypeInfo,
|
||||
PartialTypeInfo.WithoutLocationComparer.Instance)
|
||||
.Select(group => (group.Key, group.Select(e => e.FieldInfo).AsEquatableArray()));
|
||||
});
|
||||
|
||||
var hasDependencyParents = grouped
|
||||
.Collect()
|
||||
.Combine(context.CompilationProvider)
|
||||
.Select(static (a, cancel) =>
|
||||
{
|
||||
var (groups, compilation) = a;
|
||||
|
||||
var hasDependencyParents = new List<PartialTypeInfo>();
|
||||
|
||||
var ourAssemblyTypes = groups
|
||||
.Where(g => g.Item2.All(static x => !x.IsReadOnly))
|
||||
.Select(x => x.Key)
|
||||
.ToDictionary<PartialTypeInfo, INamedTypeSymbol, PartialTypeInfo>(
|
||||
x =>
|
||||
{
|
||||
var val = compilation.GetTypeByMetadataName(x.GetMetadataName());
|
||||
if (val == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
return val.OriginalDefinition;
|
||||
},
|
||||
static x => x,
|
||||
SymbolEqualityComparer.Default);
|
||||
|
||||
var hasDependencies = compilation.GetTypeByMetadataName(IHasDependenciesName);
|
||||
if (hasDependencies == null && ourAssemblyTypes.Count != 0)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
foreach (var kvp in ourAssemblyTypes)
|
||||
{
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
|
||||
var typeInfo = kvp.Key;
|
||||
|
||||
if (typeInfo.AllInterfaces.Contains(hasDependencies, SymbolEqualityComparer.Default))
|
||||
{
|
||||
hasDependencyParents.Add(kvp.Value);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (
|
||||
var ti = typeInfo.BaseType;
|
||||
ti != null && SymbolEqualityComparer.Default.Equals(ti.ContainingAssembly, compilation.Assembly);
|
||||
ti = ti.BaseType)
|
||||
{
|
||||
if (ourAssemblyTypes.ContainsKey(ti.OriginalDefinition))
|
||||
{
|
||||
hasDependencyParents.Add(kvp.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasDependencyParents.ToImmutableArray();
|
||||
});
|
||||
|
||||
context.RegisterImplementationSourceOutput(
|
||||
grouped.Combine(hasDependencyParents),
|
||||
static (productionContext, tuple) =>
|
||||
{
|
||||
var ((typeInfo, fields), hasParentList) = tuple;
|
||||
|
||||
if (fields.Any(a => a.IsReadOnly))
|
||||
return;
|
||||
|
||||
var hasParent = hasParentList.Contains(typeInfo);
|
||||
|
||||
var sb = new IndentWriter(new StringBuilder());
|
||||
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine();
|
||||
|
||||
typeInfo.WriteHeader(ref sb, "[global::Robust.Shared.IoC.HasDependenciesGeneratedAttribute]");
|
||||
|
||||
if (!hasParent)
|
||||
{
|
||||
sb.AppendLine($" : global::{IHasDependenciesName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendOpeningBrace(); // {
|
||||
|
||||
if (!hasParent && typeInfo.IsSealed)
|
||||
{
|
||||
// Explicit impl only
|
||||
sb.AppendLineIndented("[global::Robust.Shared.Analyzers.RobustAutoGenerated]");
|
||||
sb.AppendLineIndented($"global::System.Type[] global::{IHasDependenciesName}.GetDependencyTypes()");
|
||||
sb.AppendOpeningBrace(); // {
|
||||
WriteGetDependencies(ref sb, fields, false);
|
||||
sb.AppendClosingBrace(); // }
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLineIndented("[global::Robust.Shared.Analyzers.RobustAutoGenerated]");
|
||||
sb.AppendLineIndented($"void global::{IHasDependenciesName}.Inject(global::System.ReadOnlySpan<object> instances)");
|
||||
sb.AppendOpeningBrace(); // {
|
||||
WriteInject(ref sb, fields, false);
|
||||
sb.AppendClosingBrace(); // }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!hasParent)
|
||||
{
|
||||
// Explicit impl -> protected virtual methods
|
||||
sb.AppendLineIndented("[global::Robust.Shared.Analyzers.RobustAutoGenerated]");
|
||||
sb.AppendLineIndented($"global::System.Type[] global::{IHasDependenciesName}.GetDependencyTypes()");
|
||||
sb.AppendOpeningBrace(); // {
|
||||
sb.AppendLineIndented("return GetDependencyTypesImpl();");
|
||||
sb.AppendClosingBrace(); // }
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLineIndented("[global::Robust.Shared.Analyzers.RobustAutoGenerated]");
|
||||
sb.AppendLineIndented($"void global::{IHasDependenciesName}.Inject(global::System.ReadOnlySpan<object> instances)");
|
||||
sb.AppendOpeningBrace(); // {
|
||||
sb.AppendLineIndented("InjectImpl(instances);");
|
||||
sb.AppendClosingBrace(); // }
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Protected virtual/override methods
|
||||
sb.AppendLineIndented("[global::Robust.Shared.Analyzers.RobustAutoGenerated]");
|
||||
sb.AppendLineIndented("[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]");
|
||||
sb.AppendLineIndented($"protected {(hasParent ? "override" : "virtual")} global::System.Type[] GetDependencyTypesImpl()");
|
||||
sb.AppendOpeningBrace(); // {
|
||||
WriteGetDependencies(ref sb, fields, hasParent);
|
||||
sb.AppendClosingBrace(); // }
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLineIndented("[global::Robust.Shared.Analyzers.RobustAutoGenerated]");
|
||||
sb.AppendLineIndented("[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]");
|
||||
sb.AppendLineIndented($"protected {(hasParent ? "override" : "virtual")} void InjectImpl(global::System.ReadOnlySpan<object> instances)");
|
||||
sb.AppendOpeningBrace(); // {
|
||||
WriteInject(ref sb, fields, hasParent);
|
||||
sb.AppendClosingBrace(); // }
|
||||
}
|
||||
|
||||
sb.AppendClosingBrace(); // }
|
||||
|
||||
typeInfo.WriteFooter(ref sb);
|
||||
|
||||
productionContext.AddSource(typeInfo.GetGeneratedFileName(), sb.ToString());
|
||||
});
|
||||
}
|
||||
|
||||
private static void WriteGetDependencies(ref IndentWriter sb, EquatableArray<FieldInfo> fields, bool isOverride)
|
||||
{
|
||||
if (isOverride)
|
||||
{
|
||||
sb.AppendLineIndented("var baseTypes = base.GetDependencyTypesImpl();");
|
||||
sb.AppendLineIndented($"var types = new global::System.Type[baseTypes.Length + {fields.Length}];");
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
for (var i = 0; i < fields.Length; i++)
|
||||
{
|
||||
var field = fields[i];
|
||||
sb.AppendLineIndented($"types[{i}] = typeof(global::{field.TypeName});");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLineIndented($"global::System.Array.Copy(baseTypes, 0, types, {fields.Length}, baseTypes.Length);");
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLineIndented("return types;");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLineIndented("return new global::System.Type[]");
|
||||
sb.AppendOpeningBrace();
|
||||
|
||||
for (var i = 0; i < fields.Length; i++)
|
||||
{
|
||||
var field = fields[i];
|
||||
sb.AppendIndents();
|
||||
sb.Append($"typeof(global::{field.TypeName})");
|
||||
if (i != fields.Length - 1)
|
||||
sb.Append(",");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.PopDepth();
|
||||
sb.AppendLineIndented("};");
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteInject(ref IndentWriter sb, EquatableArray<FieldInfo> fields, bool isOverride)
|
||||
{
|
||||
for (var i = 0; i < fields.Length; i++)
|
||||
{
|
||||
var field = fields[i];
|
||||
sb.AppendLineIndented($"{field.Name} = (global::{field.TypeName})instances[{i}];");
|
||||
}
|
||||
|
||||
if (isOverride)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLineIndented($"base.InjectImpl(instances.Slice({fields.Length}));");
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct FieldInfo(string Name, string TypeName, bool IsReadOnly);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Robust.Roslyn.Shared;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public sealed class HasDependenciesAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DependencyAttributeName = "Robust.Shared.IoC.DependencyAttribute";
|
||||
|
||||
public static readonly DiagnosticDescriptor DiagnosticNotPartial = new(
|
||||
Diagnostics.IdHasDependenciesNotPartial,
|
||||
"Type has dependencies but is not partial",
|
||||
"Type '{0}' has [Dependency] fields but is not partial. This will be required in the future.",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true);
|
||||
|
||||
public static readonly DiagnosticDescriptor DiagnosticNotPartialParent = new(
|
||||
Diagnostics.IdHasDependenciesNotPartialParent,
|
||||
"Type has dependencies but is not in a partial type",
|
||||
"Type '{0}' has [Dependency] fields but is nested in a non-partial type. The parent being partial will be required in the future.",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true);
|
||||
|
||||
public static readonly DiagnosticDescriptor DiagnosticReadOnly = new(
|
||||
Diagnostics.IdHasDependenciesReadOnly,
|
||||
"Dependency field is readonly",
|
||||
"Field '{0}' is a [Dependency] but is readonly. This will be an error in the future.",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true);
|
||||
|
||||
public static readonly DiagnosticDescriptor DiagnosticPropertyField = new(
|
||||
Diagnostics.IdHasDependenciesPropertyField,
|
||||
"Property backing fields cannot be a dependency",
|
||||
"Property '{0}' has a backing field marked with [Dependency]. This will be an error in the future.",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
|
||||
[DiagnosticNotPartial, DiagnosticNotPartialParent, DiagnosticReadOnly, DiagnosticPropertyField];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterCompilationStartAction(static ctx =>
|
||||
{
|
||||
var attr = ctx.Compilation.GetTypeByMetadataName(DependencyAttributeName);
|
||||
if (attr == null)
|
||||
return;
|
||||
|
||||
ctx.RegisterSymbolAction(ctx => AnalyzeType(ctx, attr), SymbolKind.NamedType);
|
||||
});
|
||||
}
|
||||
|
||||
private static void AnalyzeType(SymbolAnalysisContext ctx, INamedTypeSymbol dependencyAttr)
|
||||
{
|
||||
if (ctx.Symbol is not INamedTypeSymbol typeSymbol)
|
||||
return;
|
||||
|
||||
var hasDependencies = false;
|
||||
foreach (var fieldSymbol in typeSymbol.GetMembers().OfType<IFieldSymbol>())
|
||||
{
|
||||
if (!AttributeHelper.HasAttribute(fieldSymbol, dependencyAttr, out _))
|
||||
continue;
|
||||
|
||||
hasDependencies = true;
|
||||
|
||||
if (!fieldSymbol.CanBeReferencedByName)
|
||||
{
|
||||
if (fieldSymbol.AssociatedSymbol is IPropertySymbol prop)
|
||||
{
|
||||
// I wanted to make this work, but ForAttributeWithMetadataName doesn't work with
|
||||
// backing field attributes.
|
||||
// https://github.com/dotnet/roslyn/issues/80511
|
||||
// Putting [Dependency] on the property directly isn't backwards-compatible with the old system.
|
||||
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticPropertyField,
|
||||
prop.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(),
|
||||
prop.Name));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fieldSymbol.IsReadOnly)
|
||||
{
|
||||
var fieldSyntax =
|
||||
(FieldDeclarationSyntax)fieldSymbol.DeclaringSyntaxReferences[0].GetSyntax().Parent!.Parent!;
|
||||
foreach (var modifier in fieldSyntax.Modifiers)
|
||||
{
|
||||
if (modifier.IsKind(SyntaxKind.ReadOnlyKeyword))
|
||||
{
|
||||
ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticReadOnly,
|
||||
modifier.GetLocation(),
|
||||
fieldSymbol.Name));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasDependencies)
|
||||
return;
|
||||
|
||||
var origSyntax = (TypeDeclarationSyntax)typeSymbol.DeclaringSyntaxReferences[0].GetSyntax();
|
||||
var syntax = origSyntax;
|
||||
|
||||
while (syntax != null)
|
||||
{
|
||||
var foundPartial = false;
|
||||
foreach (var modifier in syntax.Modifiers)
|
||||
{
|
||||
if (modifier.IsKind(SyntaxKind.PartialKeyword))
|
||||
{
|
||||
foundPartial = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundPartial)
|
||||
{
|
||||
var diag = syntax == origSyntax
|
||||
? DiagnosticNotPartial
|
||||
: DiagnosticNotPartialParent;
|
||||
|
||||
ctx.ReportDiagnostic(Diagnostic.Create(diag,
|
||||
origSyntax.Keyword.GetLocation(),
|
||||
typeSymbol.ToDisplayString()));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
syntax = syntax.Ancestors().OfType<TypeDeclarationSyntax>().FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Formatting;
|
||||
using Robust.Roslyn.Shared;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp)]
|
||||
public sealed class HasDependenciesCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
private const string TitleRemoveReadonly = "Remove readonly from dependency";
|
||||
private const string TitlePartial = "Add partial";
|
||||
|
||||
public override ImmutableArray<string> FixableDiagnosticIds =>
|
||||
[
|
||||
Diagnostics.IdHasDependenciesReadOnly, Diagnostics.IdHasDependenciesNotPartialParent,
|
||||
Diagnostics.IdHasDependenciesNotPartial
|
||||
];
|
||||
|
||||
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var diagnostic in context.Diagnostics)
|
||||
{
|
||||
switch (diagnostic.Id)
|
||||
{
|
||||
case Diagnostics.IdHasDependenciesReadOnly:
|
||||
{
|
||||
var diagnosticSpan = diagnostic.Location.SourceSpan;
|
||||
var declaration = root!.FindToken(diagnosticSpan.Start).Parent!.AncestorsAndSelf()
|
||||
.OfType<FieldDeclarationSyntax>()
|
||||
.First();
|
||||
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
TitleRemoveReadonly,
|
||||
c => FixReadOnlyAsync(context.Document, declaration, c),
|
||||
TitleRemoveReadonly),
|
||||
diagnostic);
|
||||
break;
|
||||
}
|
||||
case Diagnostics.IdHasDependenciesNotPartial:
|
||||
case Diagnostics.IdHasDependenciesNotPartialParent:
|
||||
{
|
||||
var diagnosticSpan = diagnostic.Location.SourceSpan;
|
||||
var declaration = root!.FindToken(diagnosticSpan.Start).Parent!.AncestorsAndSelf()
|
||||
.OfType<TypeDeclarationSyntax>()
|
||||
.First();
|
||||
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
TitlePartial,
|
||||
c => FixPartialAsync(context.Document, declaration, c),
|
||||
TitlePartial),
|
||||
diagnostic);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Document> FixReadOnlyAsync(
|
||||
Document document,
|
||||
FieldDeclarationSyntax origDeclaration,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
var readonlyModifier = origDeclaration.Modifiers.Single(m => m.IsKind(SyntaxKind.ReadOnlyKeyword));
|
||||
var newDeclaration = origDeclaration.WithModifiers(origDeclaration.Modifiers.Remove(readonlyModifier));
|
||||
var formattedField = newDeclaration.WithAdditionalAnnotations(Formatter.Annotation);
|
||||
|
||||
var oldRoot = await document.GetSyntaxRootAsync(cancel);
|
||||
var newRoot = oldRoot!.ReplaceNode(origDeclaration, formattedField);
|
||||
|
||||
return document.WithSyntaxRoot(newRoot);
|
||||
}
|
||||
|
||||
private static async Task<Document> FixPartialAsync(
|
||||
Document document,
|
||||
TypeDeclarationSyntax declaration,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
var root = (await document.GetSyntaxRootAsync(cancel))!;
|
||||
|
||||
var nodesToPartial = new List<SyntaxNode>();
|
||||
|
||||
do
|
||||
{
|
||||
if (!declaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
|
||||
nodesToPartial.Add(declaration);
|
||||
|
||||
declaration = declaration.Ancestors().OfType<TypeDeclarationSyntax>().FirstOrDefault();
|
||||
} while (declaration != null);
|
||||
|
||||
root = root.TrackNodes(nodesToPartial.ToArray());
|
||||
|
||||
foreach (var node in nodesToPartial)
|
||||
{
|
||||
var old = (TypeDeclarationSyntax)root.GetCurrentNode(node)!;
|
||||
root = root.ReplaceNode(old, old.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword)));
|
||||
}
|
||||
|
||||
return document.WithSyntaxRoot(root);
|
||||
}
|
||||
|
||||
public override FixAllProvider GetFixAllProvider()
|
||||
{
|
||||
return WellKnownFixAllProviders.BatchFixer;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,10 @@ public static class Diagnostics
|
||||
public const string IdProxyForRedundantMethodName = "RA0046";
|
||||
public const string IdProxyForTargetMethodNotFound = "RA0047";
|
||||
public const string IdDependencyNullable = "RA0048";
|
||||
public const string IdHasDependenciesNotPartial = "RA0049";
|
||||
public const string IdHasDependenciesNotPartialParent = "RA0050";
|
||||
public const string IdHasDependenciesReadOnly = "RA0051";
|
||||
public const string IdHasDependenciesPropertyField = "RA0052";
|
||||
|
||||
public static SuppressionDescriptor MeansImplicitAssignment =>
|
||||
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");
|
||||
|
||||
@@ -27,6 +27,12 @@ public static class EquatableArray
|
||||
{
|
||||
return new(array);
|
||||
}
|
||||
|
||||
public static EquatableArray<T> AsEquatableArray<T>(this IEnumerable<T> enumerable)
|
||||
where T : IEquatable<T>
|
||||
{
|
||||
return enumerable.ToImmutableArray().AsEquatableArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -61,6 +67,8 @@ public readonly struct EquatableArray<T> : IEquatable<EquatableArray<T>>, IEnume
|
||||
get => ref AsImmutableArray().ItemRef(index);
|
||||
}
|
||||
|
||||
public int Length => AsImmutableArray().Length;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the current array is empty.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Robust.Roslyn.Shared;
|
||||
|
||||
public struct IndentWriter(StringBuilder builder, int depth = 0)
|
||||
{
|
||||
public readonly StringBuilder Builder = builder;
|
||||
public int Depth = depth;
|
||||
|
||||
public readonly void AppendLine()
|
||||
{
|
||||
Builder.AppendLine();
|
||||
}
|
||||
|
||||
public readonly void AppendLine(string str)
|
||||
{
|
||||
Builder.AppendLine(str);
|
||||
}
|
||||
|
||||
public void AppendLineIndented(string str)
|
||||
{
|
||||
AppendIndents();
|
||||
Builder.AppendLine(str);
|
||||
}
|
||||
|
||||
public void AppendOpeningBrace()
|
||||
{
|
||||
AppendLineIndented("{");
|
||||
PushDepth();
|
||||
}
|
||||
|
||||
public void AppendClosingBrace()
|
||||
{
|
||||
PopDepth();
|
||||
AppendLineIndented("}");
|
||||
}
|
||||
|
||||
public readonly void AppendIndents()
|
||||
{
|
||||
Builder.Append(' ', 4 * Depth);
|
||||
}
|
||||
|
||||
public readonly void Append(string str)
|
||||
{
|
||||
Builder.Append(str);
|
||||
}
|
||||
|
||||
public void PushDepth()
|
||||
{
|
||||
Depth += 1;
|
||||
}
|
||||
|
||||
public void PopDepth()
|
||||
{
|
||||
if (Depth == 0)
|
||||
return;
|
||||
|
||||
Depth -= 1;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Builder.ToString();
|
||||
}
|
||||
}
|
||||
@@ -14,48 +14,63 @@ namespace Robust.Roslyn.Shared;
|
||||
/// </summary>
|
||||
public sealed record PartialTypeInfo(
|
||||
string? Namespace,
|
||||
string Name,
|
||||
string DisplayName,
|
||||
EquatableArray<string> TypeParameterNames,
|
||||
EquatableArray<PartialTypeInfo.NestedPart> Parts,
|
||||
bool IsValid,
|
||||
Location SyntaxLocation,
|
||||
Accessibility Accessibility,
|
||||
TypeKind Kind,
|
||||
bool IsRecord,
|
||||
bool IsAbstract)
|
||||
bool IsSealed)
|
||||
{
|
||||
public string Name => Parts[^1].Name;
|
||||
public string DisplayName => Parts[^1].DisplayName;
|
||||
|
||||
public string FullDisplayName => Namespace != null ? $"{Namespace}.{Name}" : Name;
|
||||
|
||||
public static PartialTypeInfo FromSymbol(INamedTypeSymbol symbol, TypeDeclarationSyntax syntax)
|
||||
{
|
||||
var typeParameters = ImmutableArray<string>.Empty;
|
||||
if (symbol.TypeParameters.Length > 0)
|
||||
var parts = ImmutableArray<NestedPart>.Empty.ToBuilder();
|
||||
var isValid = true;
|
||||
|
||||
var curSymbol = symbol;
|
||||
var curSyntax = syntax;
|
||||
|
||||
do
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<string>(symbol.TypeParameters.Length);
|
||||
foreach (var typeParameter in symbol.TypeParameters)
|
||||
if (!IsPartial(curSyntax))
|
||||
{
|
||||
builder.Add(typeParameter.Name);
|
||||
isValid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
typeParameters = builder.MoveToImmutable();
|
||||
}
|
||||
parts.Insert(0, NestedPart.FromNode(curSymbol, curSyntax));
|
||||
|
||||
curSymbol = curSymbol.ContainingType;
|
||||
curSyntax = curSyntax.Parent as TypeDeclarationSyntax;
|
||||
} while (curSymbol != null && curSyntax != null);
|
||||
|
||||
return new PartialTypeInfo(
|
||||
symbol.ContainingNamespace.IsGlobalNamespace ? null : symbol.ContainingNamespace.ToDisplayString(),
|
||||
symbol.Name,
|
||||
symbol.ToDisplayString(),
|
||||
typeParameters,
|
||||
syntax.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword)),
|
||||
parts.ToImmutable().AsEquatableArray(),
|
||||
isValid,
|
||||
syntax.Keyword.GetLocation(),
|
||||
symbol.DeclaredAccessibility,
|
||||
symbol.TypeKind,
|
||||
symbol.IsRecord,
|
||||
symbol.IsAbstract);
|
||||
symbol.IsSealed);
|
||||
}
|
||||
|
||||
private static bool IsPartial(TypeDeclarationSyntax syntax)
|
||||
{
|
||||
foreach (var modifier in syntax.Modifiers)
|
||||
{
|
||||
if (modifier.IsKind(SyntaxKind.PartialKeyword))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[Obsolete("Diagnostics from source generators are recommended against, apparently: https://github.com/dotnet/roslyn/issues/71709")]
|
||||
public bool CheckPartialDiagnostic(SourceProductionContext context, DiagnosticDescriptor diagnostic)
|
||||
{
|
||||
if (!IsValid)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(diagnostic, SyntaxLocation, DisplayName));
|
||||
context.ReportDiagnostic(Diagnostic.Create(diagnostic, SyntaxLocation, Parts[^1].DisplayName));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -64,9 +79,19 @@ public sealed record PartialTypeInfo(
|
||||
|
||||
public string GetGeneratedFileName()
|
||||
{
|
||||
var name = Namespace == null ? Name : $"{Namespace}.{Name}";
|
||||
if (TypeParameterNames.AsImmutableArray().Length > 0)
|
||||
name += $"`{TypeParameterNames.AsImmutableArray().Length}";
|
||||
var name = Namespace == null ? "" : $"{Namespace}.";
|
||||
|
||||
for (var index = 0; index < Parts.Length; index++)
|
||||
{
|
||||
var part = Parts[index];
|
||||
name += part.Name;
|
||||
|
||||
if (part.TypeParameterNames.Length > 0)
|
||||
name += $"`{part.TypeParameterNames.Length}";
|
||||
|
||||
if (index < Parts.Length - 1)
|
||||
name += ".";
|
||||
}
|
||||
|
||||
name += ".g.cs";
|
||||
|
||||
@@ -74,48 +99,158 @@ public sealed record PartialTypeInfo(
|
||||
}
|
||||
|
||||
public void WriteHeader(StringBuilder builder)
|
||||
{
|
||||
var writer = new IndentWriter(builder);
|
||||
WriteHeader(ref writer);
|
||||
}
|
||||
|
||||
public void WriteHeader(ref IndentWriter builder, string? attributes = null)
|
||||
{
|
||||
if (Namespace != null)
|
||||
builder.AppendLine($"namespace {Namespace};\n");
|
||||
|
||||
// TODO: Nested classes
|
||||
|
||||
var access = Accessibility switch
|
||||
for (var index = 0; index < Parts.Length; index++)
|
||||
{
|
||||
Accessibility.Private => "private",
|
||||
Accessibility.ProtectedAndInternal => "private protected",
|
||||
Accessibility.ProtectedOrInternal => "protected internal",
|
||||
Accessibility.Protected => "protected",
|
||||
Accessibility.Internal => "internal",
|
||||
_ => "public"
|
||||
};
|
||||
|
||||
string keyword;
|
||||
if (Kind == TypeKind.Interface)
|
||||
{
|
||||
keyword = "interface";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IsRecord)
|
||||
var part = Parts[index];
|
||||
var access = part.Accessibility switch
|
||||
{
|
||||
keyword = Kind == TypeKind.Struct ? "record struct" : "record";
|
||||
Accessibility.Private => "private",
|
||||
Accessibility.ProtectedAndInternal => "private protected",
|
||||
Accessibility.ProtectedOrInternal => "protected internal",
|
||||
Accessibility.Protected => "protected",
|
||||
Accessibility.Internal => "internal",
|
||||
_ => "public"
|
||||
};
|
||||
|
||||
string keyword;
|
||||
if (part.Kind == TypeKind.Interface)
|
||||
{
|
||||
keyword = "interface";
|
||||
}
|
||||
else
|
||||
{
|
||||
keyword = Kind == TypeKind.Struct ? "struct" : "class";
|
||||
if (part.IsRecord)
|
||||
{
|
||||
keyword = part.Kind == TypeKind.Struct ? "record struct" : "record";
|
||||
}
|
||||
else
|
||||
{
|
||||
keyword = part.Kind == TypeKind.Struct ? "struct" : "class";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append($"{access} {(IsAbstract ? "abstract " : "")}partial {keyword} {Name}");
|
||||
if (TypeParameterNames.AsSpan().Length > 0)
|
||||
{
|
||||
builder.Append($"<{string.Join(", ", TypeParameterNames.AsImmutableArray())}>");
|
||||
if (attributes != null && index == Parts.Length - 1)
|
||||
builder.AppendLineIndented(attributes);
|
||||
|
||||
builder.AppendIndents();
|
||||
builder.Append($"{access} {(part.IsAbstract ? "abstract " : "")}partial {keyword} {part.Name}");
|
||||
if (part.TypeParameterNames.Length > 0)
|
||||
{
|
||||
builder.Append($"<{string.Join(", ", part.TypeParameterNames.AsImmutableArray())}>");
|
||||
}
|
||||
|
||||
if (index != Parts.Length - 1)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.AppendOpeningBrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteFooter(StringBuilder builder)
|
||||
{
|
||||
// TODO: Nested classes
|
||||
var writer = new IndentWriter(builder);
|
||||
WriteFooter(ref writer);
|
||||
}
|
||||
|
||||
public void WriteFooter(ref IndentWriter builder)
|
||||
{
|
||||
// Loop starts at 1, only write for nested classes.
|
||||
for (var i = 1; i < Parts.Length; i++)
|
||||
{
|
||||
builder.AppendClosingBrace();
|
||||
}
|
||||
}
|
||||
|
||||
public string GetMetadataName()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (Namespace != null)
|
||||
{
|
||||
sb.Append(Namespace);
|
||||
sb.Append('.');
|
||||
}
|
||||
|
||||
for (var i = 0; i < Parts.Length; i++)
|
||||
{
|
||||
var part = Parts[i];
|
||||
sb.Append(part.MetadataName);
|
||||
if (i != Parts.Length - 1)
|
||||
sb.Append('+');
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public sealed record NestedPart(
|
||||
string Name,
|
||||
string MetadataName,
|
||||
string DisplayName,
|
||||
EquatableArray<string> TypeParameterNames,
|
||||
Accessibility Accessibility,
|
||||
TypeKind Kind,
|
||||
bool IsRecord,
|
||||
bool IsAbstract)
|
||||
{
|
||||
public static NestedPart FromNode(INamedTypeSymbol symbol, TypeDeclarationSyntax syntax)
|
||||
{
|
||||
var typeParameters = ImmutableArray<string>.Empty;
|
||||
if (symbol.TypeParameters.Length > 0)
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<string>(symbol.TypeParameters.Length);
|
||||
foreach (var typeParameter in symbol.TypeParameters)
|
||||
{
|
||||
builder.Add(typeParameter.Name);
|
||||
}
|
||||
|
||||
typeParameters = builder.MoveToImmutable();
|
||||
}
|
||||
|
||||
return new NestedPart(
|
||||
symbol.Name,
|
||||
symbol.MetadataName,
|
||||
symbol.ToDisplayString(),
|
||||
typeParameters,
|
||||
symbol.DeclaredAccessibility,
|
||||
symbol.TypeKind,
|
||||
symbol.IsRecord,
|
||||
symbol.IsAbstract);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WithoutLocationComparer : IEqualityComparer<PartialTypeInfo>
|
||||
{
|
||||
public static readonly WithoutLocationComparer Instance = new();
|
||||
|
||||
public bool Equals(PartialTypeInfo? x, PartialTypeInfo? y)
|
||||
{
|
||||
if (ReferenceEquals(x, y)) return true;
|
||||
if (x is null) return false;
|
||||
if (y is null) return false;
|
||||
if (x.GetType() != y.GetType()) return false;
|
||||
return x.Namespace == y.Namespace && x.Parts.Equals(y.Parts) && x.IsValid == y.IsValid;
|
||||
}
|
||||
|
||||
public int GetHashCode(PartialTypeInfo obj)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var hashCode = (obj.Namespace != null ? obj.Namespace.GetHashCode() : 0);
|
||||
hashCode = (hashCode * 397) ^ obj.Parts.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ obj.IsValid.GetHashCode();
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ namespace Robust.Shared.IoC
|
||||
|
||||
private static readonly Type[] InjectorParameters = { typeof(object), typeof(object[]) };
|
||||
|
||||
// Temporary: cache of whether IHasDependencies is safe to use on a type.
|
||||
// False for any types on which the source gen has failed to run.
|
||||
private static readonly Dictionary<Type, bool> IsHasDependenciesSafe = new();
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary that maps the types passed to <see cref="Resolve{T}()"/> to their implementation.
|
||||
/// This is the first dictionary to get hit on a resolve.
|
||||
@@ -480,7 +484,7 @@ namespace Robust.Shared.IoC
|
||||
// Graph built, go over ones that need injection.
|
||||
foreach (var implementation in injectList)
|
||||
{
|
||||
InjectDependenciesReflection(implementation, _services);
|
||||
InjectDependenciesReflection(implementation);
|
||||
}
|
||||
|
||||
foreach (var injectedItem in injectList.OfType<IPostInjectInit>())
|
||||
@@ -512,23 +516,51 @@ namespace Robust.Shared.IoC
|
||||
return;
|
||||
}
|
||||
|
||||
injector = CacheInjector(type);
|
||||
injector = CacheInjector(obj, type);
|
||||
}
|
||||
|
||||
var (@delegate, services) = injector;
|
||||
var (@delegate, hasDependencies, services) = injector;
|
||||
|
||||
if (services?.Length == 0)
|
||||
return;
|
||||
|
||||
if (hasDependencies)
|
||||
{
|
||||
((IHasDependencies)obj).Inject(services);
|
||||
}
|
||||
|
||||
// If @delegate is null then the type has no dependencies.
|
||||
// So running an initializer would be quite wasteful.
|
||||
@delegate?.Invoke(obj, services!);
|
||||
}
|
||||
|
||||
private void InjectDependenciesReflection(object obj)
|
||||
private object ResolveForInjection(Type owningType, Type fieldType, FrozenDictionary<Type, object> services)
|
||||
{
|
||||
InjectDependenciesReflection(obj, _services);
|
||||
// Not using Resolve<T>() because we're literally building it right now.
|
||||
if (TryResolveType(fieldType, services, out var dep))
|
||||
{
|
||||
// Quick note: this DOES work with read only fields, though it may be a CLR implementation detail.
|
||||
return dep;
|
||||
}
|
||||
|
||||
// A hard-coded special case so the DependencyCollection can inject itself.
|
||||
// This is not put into the services so it can be overridden if needed.
|
||||
if (fieldType == typeof(IDependencyCollection))
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
throw new UnregisteredDependencyException(owningType, fieldType);
|
||||
}
|
||||
|
||||
private void InjectDependenciesReflection(object obj, FrozenDictionary<Type, object> services)
|
||||
private void InjectDependenciesReflection(object obj)
|
||||
{
|
||||
if (CalculateHasDependenciesSafe(obj.GetType()))
|
||||
{
|
||||
InjectImmediateHasDependencies(obj);
|
||||
return;
|
||||
}
|
||||
|
||||
var type = obj.GetType();
|
||||
foreach (var field in type.GetAllFields())
|
||||
{
|
||||
@@ -537,33 +569,30 @@ namespace Robust.Shared.IoC
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not using Resolve<T>() because we're literally building it right now.
|
||||
if (TryResolveType(field.FieldType, services, out var dep))
|
||||
{
|
||||
// Quick note: this DOES work with read only fields, though it may be a CLR implementation detail.
|
||||
field.SetValue(obj, dep);
|
||||
continue;
|
||||
}
|
||||
|
||||
// A hard-coded special case so the DependencyCollection can inject itself.
|
||||
// This is not put into the services so it can be overridden if needed.
|
||||
if (field.FieldType == typeof(IDependencyCollection))
|
||||
{
|
||||
field.SetValue(obj, this);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new UnregisteredDependencyException(type, field.FieldType, field.Name);
|
||||
field.SetValue(obj, ResolveForInjection(type, field.FieldType, _services));
|
||||
}
|
||||
}
|
||||
|
||||
private CachedInjector CacheInjector(Type type)
|
||||
private void InjectImmediateHasDependencies(object obj)
|
||||
{
|
||||
if (obj is not IHasDependencies hasDependencies)
|
||||
return;
|
||||
|
||||
var types = hasDependencies.GetDependencyTypes();
|
||||
var services = ResolveServicesArray(obj.GetType(), types);
|
||||
hasDependencies.Inject(services);
|
||||
}
|
||||
|
||||
private CachedInjector CacheInjector(object obj, Type type)
|
||||
{
|
||||
using var _ = _injectorCacheLock.WriteGuard();
|
||||
// Check in case value got filled in right before we acquired the lock.
|
||||
if (_injectorCache.TryGetValue(type, out var cached))
|
||||
return cached;
|
||||
|
||||
if (CalculateHasDependenciesSafe(type))
|
||||
return CacheInjectorHasDependencies(obj, type);
|
||||
|
||||
var fields = new List<FieldInfo>();
|
||||
|
||||
foreach (var field in type.GetAllFields())
|
||||
@@ -628,12 +657,37 @@ namespace Robust.Shared.IoC
|
||||
generator.Emit(OpCodes.Ret);
|
||||
|
||||
var @delegate = (InjectorDelegate)dynamicMethod.CreateDelegate(typeof(InjectorDelegate));
|
||||
cached = new CachedInjector(@delegate, services.ToArray());
|
||||
cached = new CachedInjector(@delegate, false, services.ToArray());
|
||||
_injectorCache.Add(type, cached);
|
||||
|
||||
return cached;
|
||||
}
|
||||
|
||||
private CachedInjector CacheInjectorHasDependencies(object obj, Type type)
|
||||
{
|
||||
DebugTools.Assert(type == obj.GetType());
|
||||
|
||||
if (obj is not IHasDependencies hasDeps)
|
||||
return new CachedInjector(null, true, []);
|
||||
|
||||
var types = hasDeps.GetDependencyTypes();
|
||||
var services = ResolveServicesArray(type, types);
|
||||
|
||||
return new CachedInjector(null, true, services);
|
||||
}
|
||||
|
||||
private object[] ResolveServicesArray(Type owningType, Type[] types)
|
||||
{
|
||||
var result = new object[types.Length];
|
||||
|
||||
for (var i = 0; i < types.Length; i++)
|
||||
{
|
||||
result[i] = ResolveForInjection(owningType, types[i], _services);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[return: NotNullIfNotNull("factory")]
|
||||
private static DependencyFactoryDelegateInternal<T>? FactoryToInternal<T>(
|
||||
DependencyFactoryDelegate<T>? factory)
|
||||
@@ -645,6 +699,41 @@ namespace Robust.Shared.IoC
|
||||
return _ => factory();
|
||||
}
|
||||
|
||||
private record struct CachedInjector(InjectorDelegate? Delegate, object[]? Services);
|
||||
private record struct CachedInjector(InjectorDelegate? Delegate, bool HasDependencies, object[]? Services);
|
||||
|
||||
private static bool HasAnyDependenciesAtLevel(Type type)
|
||||
{
|
||||
return type
|
||||
.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)
|
||||
.Any(field => field.HasCustomAttribute<DependencyAttribute>());
|
||||
}
|
||||
|
||||
private static bool CalculateHasDependenciesSafe(Type type)
|
||||
{
|
||||
bool safe;
|
||||
|
||||
lock (IsHasDependenciesSafe)
|
||||
{
|
||||
if (IsHasDependenciesSafe.TryGetValue(type, out safe))
|
||||
return safe;
|
||||
|
||||
safe = true;
|
||||
for (var checkType = type; checkType != null; checkType = checkType.BaseType)
|
||||
{
|
||||
if (checkType.HasCustomAttribute<HasDependenciesGeneratedAttribute>())
|
||||
continue;
|
||||
|
||||
if (HasAnyDependenciesAtLevel(checkType))
|
||||
{
|
||||
safe = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
IsHasDependenciesSafe.Add(type, safe);
|
||||
}
|
||||
|
||||
return safe;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,5 +34,13 @@ namespace Robust.Shared.IoC.Exceptions
|
||||
TargetType = target.AssemblyQualifiedName;
|
||||
FieldName = fieldName;
|
||||
}
|
||||
|
||||
public UnregisteredDependencyException(Type owner, Type target)
|
||||
: base($"{owner} requested unregistered type with a dependency field: {target}")
|
||||
{
|
||||
OwnerType = owner.AssemblyQualifiedName;
|
||||
TargetType = target.AssemblyQualifiedName;
|
||||
FieldName = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Robust.Shared.IoC;
|
||||
|
||||
/// <summary>
|
||||
/// Declares that a type has dependencies that can be injected via <see cref="IDependencyCollection"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This type should only be used by engine.
|
||||
/// The API may change in the future so content should not implement or use it itself.
|
||||
/// Automatic implementation via the source generator is the only supported method, this is not ABI stable.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
[RequiresExplicitImplementation]
|
||||
[NotContentImplementable]
|
||||
public interface IHasDependencies
|
||||
{
|
||||
/// <summary>
|
||||
/// Get an array of types that this object wants to have resolved and injected.
|
||||
/// </summary>
|
||||
Type[] GetDependencyTypes();
|
||||
|
||||
/// <summary>
|
||||
/// Inject services into this type.
|
||||
/// </summary>
|
||||
/// <param name="instances">
|
||||
/// The list of services to inject.
|
||||
/// This is the same length and order as the types returned by <see cref="GetDependencyTypes"/>.
|
||||
/// </param>
|
||||
void Inject(ReadOnlySpan<object> instances);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Has the dependencies source generator ran on this type?
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If this attribute is lacking on a type that has <c>[Dependency]</c> fields,
|
||||
/// it indicates that the type in question needs its code fixed to support the source generator.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This type is a TEMPORARY implementation detail for backwards compatibility.
|
||||
/// It will be removed in the future along with support for non-source-generated dependencies.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public sealed class HasDependenciesGeneratedAttribute : Attribute;
|
||||
Reference in New Issue
Block a user