未验证 提交 e6afc959 编写于 作者: C Chris Sienkiewicz 提交者: GitHub

Sg embedded sources (#44768)

Embed generated source texts in the portable PDB:
- Allows stepping in etc. in the IDE as well as a dump debugging after compilation
- Consistent ordering of generated syntax trees.
- Unique hint names
- Restrict hint names to file path legal characters
- Add tests
上级 d1532d6e
Microsoft.CodeAnalysis.CSharp.CSharpGeneratorDriver
Microsoft.CodeAnalysis.CSharp.CSharpGeneratorDriver.CSharpGeneratorDriver(Microsoft.CodeAnalysis.ParseOptions parseOptions, System.Collections.Immutable.ImmutableArray<Microsoft.CodeAnalysis.ISourceGenerator> generators, System.Collections.Immutable.ImmutableArray<Microsoft.CodeAnalysis.AdditionalText> additionalTexts) -> void
Microsoft.CodeAnalysis.CSharp.Syntax.FunctionPointerTypeSyntax
Microsoft.CodeAnalysis.CSharp.Syntax.FunctionPointerTypeSyntax.AddParameters(params Microsoft.CodeAnalysis.CSharp.Syntax.ParameterSyntax[] items) -> Microsoft.CodeAnalysis.CSharp.Syntax.FunctionPointerTypeSyntax
Microsoft.CodeAnalysis.CSharp.Syntax.FunctionPointerTypeSyntax.AsteriskToken.get -> Microsoft.CodeAnalysis.SyntaxToken
......
......@@ -17,7 +17,6 @@
#nullable enable
namespace Microsoft.CodeAnalysis.CSharp
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0016:Add public types and members to the declared API", Justification = "In Progress")]
public sealed class CSharpGeneratorDriver : GeneratorDriver
{
public CSharpGeneratorDriver(ParseOptions parseOptions, ImmutableArray<ISourceGenerator> generators, ImmutableArray<AdditionalText> additionalTexts)
......@@ -30,8 +29,8 @@ private CSharpGeneratorDriver(GeneratorDriverState state)
{
}
internal override SyntaxTree ParseGeneratedSourceText(GeneratedSourceText input, CancellationToken cancellationToken)
=> SyntaxFactory.ParseSyntaxTree(input.Text, _state.ParseOptions, input.HintName, cancellationToken); // https://github.com/dotnet/roslyn/issues/42628: hint path/ filename uniqueness
internal override SyntaxTree ParseGeneratedSourceText(GeneratedSourceText input, string fileName, CancellationToken cancellationToken)
=> SyntaxFactory.ParseSyntaxTree(input.Text, _state.ParseOptions, fileName, cancellationToken);
internal override GeneratorDriver FromState(GeneratorDriverState state) => new CSharpGeneratorDriver(state);
......
......@@ -2399,7 +2399,6 @@ private static void ValidateEmbeddedSources_Portable(Dictionary<string, string>
continue;
}
Assert.True(embeddedSource.Encoding is UTF8Encoding && embeddedSource.Encoding.GetPreamble().Length == 0);
Assert.Equal(expectedEmbeddedMap[docPath], embeddedSource.ToString());
Assert.True(expectedEmbeddedMap.Remove(docPath));
}
......@@ -12194,6 +12193,58 @@ public void TestAnalyzerFilteringBasedOnSeverity(DiagnosticSeverity defaultSever
}
}
[Fact]
public void SourceGenerators_EmbeddedSources()
{
var dir = Temp.CreateDirectory();
var src = dir.CreateFile("temp.cs").WriteAllText(@"
class C
{
}");
var generatedSource = "public class D { }";
var generator = new SimpleGenerator(generatedSource, "generatedSource.cs");
VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/langversion:preview", "/debug:embedded", "/out:embed.exe" }, generators: new[] { generator }, analyzers: null);
var generatorPrefix = $"{generator.GetType().Module.ModuleVersionId}_{generator.GetType().FullName}";
ValidateEmbeddedSources_Portable(new Dictionary<string, string> { { Path.Combine(dir.Path, $"{generatorPrefix}_generatedSource.cs"), generatedSource } }, dir, true);
// Clean up temp files
CleanupAllGeneratedFiles(src.Path);
}
[Theory]
[InlineData("partial class D {}", "file1.cs", "partial class E {}", "file2.cs")] // different files, different names
[InlineData("partial class D {}", "file1.cs", "partial class E {}", "file1.cs")] // different files, same names
[InlineData("partial class D {}", "file1.cs", "partial class D {}", "file2.cs")] // same files, different names
[InlineData("partial class D {}", "file1.cs", "partial class D {}", "file1.cs")] // same files, same names
[InlineData("partial class D {}", "file1.cs", "", "file2.cs")] // empty second file
public void SourceGenerators_EmbeddedSources_MultipleFiles(string source1, string source1Name, string source2, string source2Name)
{
var dir = Temp.CreateDirectory();
var src = dir.CreateFile("temp.cs").WriteAllText(@"
class C
{
}");
var generator = new SimpleGenerator(source1, source1Name);
var generator2 = new SimpleGenerator2(source2, source2Name);
VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/langversion:preview", "/debug:embedded", "/out:embed.exe" }, generators: new[] { generator, generator2 }, analyzers: null);
var generator1Prefix = $"{generator.GetType().Module.ModuleVersionId}_{generator.GetType().FullName}";
var generator2Prefix = $"{generator2.GetType().Module.ModuleVersionId}_{generator2.GetType().FullName}";
ValidateEmbeddedSources_Portable(new Dictionary<string, string>
{
{ $"{dir.Path}{Path.DirectorySeparatorChar}{generator1Prefix}_{source1Name}", source1},
{ $"{dir.Path}{Path.DirectorySeparatorChar}{generator2Prefix}_{source2Name}", source2},
}, dir, true);
// Clean up temp files
CleanupAllGeneratedFiles(src.Path);
}
[Fact]
[WorkItem(44087, "https://github.com/dotnet/roslyn/issues/44087")]
public void SourceGeneratorsAndAnalyzerConfig()
......@@ -12207,9 +12258,9 @@ class C
[*.cs]
key = value");
var generator = new SimpleGenerator("public class D {}");
var generator = new SimpleGenerator("public class D {}", "generated.cs");
VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/langversion:preview", "/analyzerconfig:" + analyzerConfig.Path }, generators: new[] { generator }, analyzers: new HiddenDiagnosticAnalyzer());
VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/langversion:preview", "/analyzerconfig:" + analyzerConfig.Path }, generators: new[] { generator }, analyzers: null);
}
[DiagnosticAnalyzer(LanguageNames.CSharp)]
......@@ -12455,6 +12506,7 @@ public override void CreateAnalyzerWithinCompilation(CompilationStartAnalysisCon
internal class SimpleGenerator : ISourceGenerator
{
private readonly string _sourceToAdd;
private readonly string _fileName;
/// <remarks>
/// Required for reflection based tests
......@@ -12464,21 +12516,26 @@ public SimpleGenerator()
_sourceToAdd = string.Empty;
}
public SimpleGenerator(string sourceToAdd)
public SimpleGenerator(string sourceToAdd, string fileName)
{
_sourceToAdd = sourceToAdd;
_fileName = fileName;
}
public void Execute(SourceGeneratorContext context)
{
if (!string.IsNullOrWhiteSpace(_sourceToAdd))
{
context.AddSource("addedSource.cs", SourceText.From(_sourceToAdd, Encoding.UTF8));
}
context.AddSource(_fileName, SourceText.From(_sourceToAdd, Encoding.UTF8));
}
public void Initialize(InitializationContext context)
{
}
}
internal class SimpleGenerator2 : SimpleGenerator
{
public SimpleGenerator2(string sourceToAdd, string fileName) : base(sourceToAdd, fileName)
{
}
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Xunit;
#nullable enable
namespace Microsoft.CodeAnalysis.CSharp.Semantic.UnitTests.SourceGeneration
{
public class AdditionalSourcesCollectionTests
: CSharpTestBase
{
[Theory]
[InlineData("abc")] // abc.cs
[InlineData("abc.cs")] //abc.cs
[InlineData("abc.vb")] // abc.vb.cs
[InlineData("abc.generated.cs")]
[InlineData("abc_-_")]
[InlineData("abc - generated.cs")]
[InlineData("abc\u0064.cs")] //acbd.cs
[InlineData("abc(1).cs")]
[InlineData("abc[1].cs")]
[InlineData("abc{1}.cs")]
public void HintName_ValidValues(string hintName)
{
AdditionalSourcesCollection asc = new AdditionalSourcesCollection();
asc.Add(hintName, SourceText.From("public class D{}", Encoding.UTF8));
Assert.True(asc.Contains(hintName));
var sources = asc.ToImmutableAndFree();
Assert.True(sources[0].HintName.EndsWith(".cs"));
}
[Theory]
[InlineData("/abc/def.cs")]
[InlineData("\\")]
[InlineData(":")]
[InlineData("*")]
[InlineData("?")]
[InlineData("\"")]
[InlineData("<")]
[InlineData(">")]
[InlineData("|")]
[InlineData("\0")]
[InlineData("abc\u00A0.cs")] // unicode non-breaking space
public void HintName_InvalidValues(string hintName)
{
AdditionalSourcesCollection asc = new AdditionalSourcesCollection();
Assert.Throws<ArgumentException>(nameof(hintName), () => asc.Add(hintName, SourceText.From("public class D{}", Encoding.UTF8)));
}
[Fact]
public void AddedSources_Are_Deterministic()
{
// a few manual simple ones
AdditionalSourcesCollection asc = new AdditionalSourcesCollection();
asc.Add("file3.cs", SourceText.From("", Encoding.UTF8));
asc.Add("file1.cs", SourceText.From("", Encoding.UTF8));
asc.Add("file2.cs", SourceText.From("", Encoding.UTF8));
asc.Add("file5.cs", SourceText.From("", Encoding.UTF8));
asc.Add("file4.cs", SourceText.From("", Encoding.UTF8));
var sources = asc.ToImmutableAndFree();
var hintNames = sources.Select(s => s.HintName).ToArray();
Assert.Equal(new[]
{
"file3.cs",
"file1.cs",
"file2.cs",
"file5.cs",
"file4.cs"
}, hintNames);
// generate a long random list, remembering the order we added them
Random r = new Random();
string[] names = new string[1000];
asc = new AdditionalSourcesCollection();
for (int i = 0; i < 1000; i++)
{
names[i] = r.NextDouble().ToString() + ".cs";
asc.Add(names[i], SourceText.From("", Encoding.UTF8));
}
sources = asc.ToImmutableAndFree();
hintNames = sources.Select(s => s.HintName).ToArray();
Assert.Equal(names, hintNames);
}
[Theory]
[InlineData("file.cs", "file.cs")]
[InlineData("file", "file")]
[InlineData("file", "file.cs")]
[InlineData("file.cs", "file")]
[InlineData("file.cs", "file.CS")]
[InlineData("file.CS", "file.cs")]
[InlineData("file", "file.CS")]
[InlineData("file.CS", "file")]
[InlineData("File", "file")]
[InlineData("file", "File")]
public void Hint_Name_Must_Be_Unique(string hintName1, string hintName2)
{
AdditionalSourcesCollection asc = new AdditionalSourcesCollection();
asc.Add(hintName1, SourceText.From("", Encoding.UTF8));
Assert.Throws<ArgumentException>("hintName", () => asc.Add(hintName2, SourceText.From("", Encoding.UTF8)));
}
[Theory]
[InlineData("file.cs", "file.cs")]
[InlineData("file", "file")]
[InlineData("file", "file.cs")]
[InlineData("file.cs", "file")]
[InlineData("file.CS", "file")]
[InlineData("file", "file.CS")]
[InlineData("File", "file.cs")]
[InlineData("File.cs", "file")]
[InlineData("File.cs", "file.CS")]
public void Contains(string addHintName, string checkHintName)
{
AdditionalSourcesCollection asc = new AdditionalSourcesCollection();
asc.Add(addHintName, SourceText.From("", Encoding.UTF8));
Assert.True(asc.Contains(checkHintName));
}
[Theory]
[InlineData("file.cs", "file.cs")]
[InlineData("file", "file")]
[InlineData("file", "file.cs")]
[InlineData("file.cs", "file")]
public void Remove(string addHintName, string removeHintName)
{
AdditionalSourcesCollection asc = new AdditionalSourcesCollection();
asc.Add(addHintName, SourceText.From("", Encoding.UTF8));
asc.RemoveSource(removeHintName);
var sources = asc.ToImmutableAndFree();
Assert.Empty(sources);
}
}
}
......@@ -99,7 +99,7 @@ class C { }
Assert.Single(compilation.SyntaxTrees);
int initCount = 0, executeCount = 0;
var generator = new CallbackGenerator((ic) => initCount++, (sgc) => executeCount++, sourceOpt: "public class C { }");
var generator = new CallbackGenerator((ic) => initCount++, (sgc) => executeCount++, source: "public class C { }");
GeneratorDriver driver = new CSharpGeneratorDriver(parseOptions, ImmutableArray.Create<ISourceGenerator>(generator), ImmutableArray<AdditionalText>.Empty);
driver = driver.RunFullGeneration(compilation, out var outputCompilation, out _);
......@@ -266,7 +266,7 @@ class C { }
Assert.Single(outputCompilation.SyntaxTrees);
// create an edit
AdditionalFileAddedEdit edit = new AdditionalFileAddedEdit(new InMemoryAdditionalText("c:\\a\\file2.cs", ""));
AdditionalFileAddedEdit edit = new AdditionalFileAddedEdit(new InMemoryAdditionalText("a\\file2.cs", ""));
driver = driver.WithPendingEdits(ImmutableArray.Create<PendingEdit>(edit));
// now try apply edits (which will fail)
......@@ -297,7 +297,7 @@ class C { }
Assert.Single(outputCompilation.SyntaxTrees);
// create an edit
AdditionalFileAddedEdit edit = new AdditionalFileAddedEdit(new InMemoryAdditionalText("c:\\a\\file1.cs", ""));
AdditionalFileAddedEdit edit = new AdditionalFileAddedEdit(new InMemoryAdditionalText("a\\file1.cs", ""));
driver = driver.WithPendingEdits(ImmutableArray.Create<PendingEdit>(edit));
// now try apply edits
......@@ -322,14 +322,14 @@ class C { }
GeneratorDriver driver = new CSharpGeneratorDriver(parseOptions,
generators: ImmutableArray.Create<ISourceGenerator>(testGenerator),
additionalTexts: ImmutableArray.Create<AdditionalText>(new InMemoryAdditionalText("c:\\a\\file1.cs", "")));
additionalTexts: ImmutableArray.Create<AdditionalText>(new InMemoryAdditionalText("a\\file1.cs", "")));
// run initial generation pass
driver = driver.RunFullGeneration(compilation, out var outputCompilation, out _);
Assert.Equal(2, outputCompilation.SyntaxTrees.Count());
// create an edit
AdditionalFileAddedEdit edit = new AdditionalFileAddedEdit(new InMemoryAdditionalText("c:\\a\\file2.cs", ""));
AdditionalFileAddedEdit edit = new AdditionalFileAddedEdit(new InMemoryAdditionalText("a\\file2.cs", ""));
driver = driver.WithPendingEdits(ImmutableArray.Create<PendingEdit>(edit));
// now try apply edits
......@@ -342,9 +342,9 @@ class C { }
Assert.Equal(3, outputCompilation.SyntaxTrees.Count());
// lets add multiple edits
driver = driver.WithPendingEdits(ImmutableArray.Create<PendingEdit>(new AdditionalFileAddedEdit(new InMemoryAdditionalText("c:\\a\\file3.cs", "")),
new AdditionalFileAddedEdit(new InMemoryAdditionalText("c:\\a\\file4.cs", "")),
new AdditionalFileAddedEdit(new InMemoryAdditionalText("c:\\a\\file5.cs", ""))));
driver = driver.WithPendingEdits(ImmutableArray.Create<PendingEdit>(new AdditionalFileAddedEdit(new InMemoryAdditionalText("a\\file3.cs", "")),
new AdditionalFileAddedEdit(new InMemoryAdditionalText("a\\file4.cs", "")),
new AdditionalFileAddedEdit(new InMemoryAdditionalText("a\\file5.cs", ""))));
// now try apply edits
driver = driver.TryApplyEdits(compilation, out editedCompilation, out succeeded);
Assert.True(succeeded);
......@@ -364,11 +364,11 @@ class C { }
Assert.Single(compilation.SyntaxTrees);
AdditionalFileAddedGenerator testGenerator = new AdditionalFileAddedGenerator();
var text = new InMemoryAdditionalText("c:\\a\\file1.cs", "");
var text = new InMemoryAdditionalText("a\\file1.cs", "");
GeneratorDriver driver = new CSharpGeneratorDriver(parseOptions,
generators: ImmutableArray.Create<ISourceGenerator>(testGenerator),
additionalTexts: ImmutableArray.Create<AdditionalText>(new InMemoryAdditionalText("c:\\a\\file1.cs", "")));
additionalTexts: ImmutableArray.Create<AdditionalText>(new InMemoryAdditionalText("a\\file1.cs", "")));
driver = driver.RunFullGeneration(compilation, out var outputCompilation, out _);
......@@ -384,7 +384,7 @@ class C { }
Assert.Equal(2, outputCompilation.SyntaxTrees.Count());
// create an edit
AdditionalFileAddedEdit edit = new AdditionalFileAddedEdit(new InMemoryAdditionalText("c:\\a\\file2.cs", ""));
AdditionalFileAddedEdit edit = new AdditionalFileAddedEdit(new InMemoryAdditionalText("a\\file2.cs", ""));
driver = driver.WithPendingEdits(ImmutableArray.Create<PendingEdit>(edit));
// now try apply edits
......@@ -410,19 +410,19 @@ class C { }
Assert.Single(compilation.SyntaxTrees);
AdditionalFileAddedGenerator testGenerator = new AdditionalFileAddedGenerator();
var text = new InMemoryAdditionalText("c:\\a\\file1.cs", "");
var text = new InMemoryAdditionalText("a\\file1.cs", "");
GeneratorDriver driver = new CSharpGeneratorDriver(parseOptions,
generators: ImmutableArray.Create<ISourceGenerator>(testGenerator),
additionalTexts: ImmutableArray.Create<AdditionalText>(new InMemoryAdditionalText("c:\\a\\file1.cs", "")));
additionalTexts: ImmutableArray.Create<AdditionalText>(new InMemoryAdditionalText("a\\file1.cs", "")));
driver.RunFullGeneration(compilation, out var outputCompilation, out _);
Assert.Equal(2, outputCompilation.SyntaxTrees.Count());
// add multiple edits
driver = driver.WithPendingEdits(ImmutableArray.Create<PendingEdit>(new AdditionalFileAddedEdit(new InMemoryAdditionalText("c:\\a\\file2.cs", "")),
new AdditionalFileAddedEdit(new InMemoryAdditionalText("c:\\a\\file3.cs", "")),
new AdditionalFileAddedEdit(new InMemoryAdditionalText("c:\\a\\file4.cs", ""))));
driver = driver.WithPendingEdits(ImmutableArray.Create<PendingEdit>(new AdditionalFileAddedEdit(new InMemoryAdditionalText("a\\file2.cs", "")),
new AdditionalFileAddedEdit(new InMemoryAdditionalText("a\\file3.cs", "")),
new AdditionalFileAddedEdit(new InMemoryAdditionalText("a\\file4.cs", ""))));
// but just do a full generation (don't try apply)
driver.RunFullGeneration(compilation, out outputCompilation, out _);
......@@ -442,7 +442,7 @@ class C { }
Assert.Single(compilation.SyntaxTrees);
SingleFileTestGenerator testGenerator1 = new SingleFileTestGenerator("public class D { }");
SingleFileTestGenerator testGenerator2 = new SingleFileTestGenerator("public class E { }");
SingleFileTestGenerator2 testGenerator2 = new SingleFileTestGenerator2("public class E { }");
GeneratorDriver driver = new CSharpGeneratorDriver(parseOptions,
generators: ImmutableArray.Create<ISourceGenerator>(testGenerator1),
......@@ -510,7 +510,7 @@ class C { }
Assert.Single(compilation.SyntaxTrees);
var exception = new InvalidOperationException("init error");
var generator = new CallbackGenerator((ic) => throw exception, (sgc) => { }, sourceOpt: "class D { }");
var generator = new CallbackGenerator((ic) => throw exception, (sgc) => { }, source: "class D { }");
GeneratorDriver driver = new CSharpGeneratorDriver(parseOptions, ImmutableArray.Create<ISourceGenerator>(generator), ImmutableArray<AdditionalText>.Empty);
driver.RunFullGeneration(compilation, out var outputCompilation, out _);
......@@ -559,7 +559,7 @@ class C { }
var exception = new InvalidOperationException("generate error");
var generator = new CallbackGenerator((ic) => { }, (sgc) => throw exception);
var generator2 = new CallbackGenerator((ic) => { }, (sgc) => { }, sourceOpt: "public class D { }");
var generator2 = new CallbackGenerator2((ic) => { }, (sgc) => { }, source: "public class D { }");
GeneratorDriver driver = new CSharpGeneratorDriver(parseOptions, ImmutableArray.Create<ISourceGenerator>(generator, generator2), ImmutableArray<AdditionalText>.Empty);
driver.RunFullGeneration(compilation, out var outputCompilation, out var generatorDiagnostics);
......@@ -595,7 +595,7 @@ class C
var exception = new InvalidOperationException("generate error");
var generator = new CallbackGenerator((ic) => { }, (sgc) => throw exception, sourceOpt: "public class D { }");
var generator = new CallbackGenerator((ic) => { }, (sgc) => throw exception, source: "public class D { }");
GeneratorDriver driver = new CSharpGeneratorDriver(parseOptions, ImmutableArray.Create<ISourceGenerator>(generator), ImmutableArray<AdditionalText>.Empty);
driver.RunFullGeneration(compilation, out var outputCompilation, out var generatorDiagnostics);
......@@ -636,99 +636,62 @@ class C { }
Diagnostic("TG001").WithLocation(1, 1)
);
}
}
internal class SingleFileTestGenerator : ISourceGenerator
{
private readonly string _content;
private readonly string _hintName;
public SingleFileTestGenerator(string content, string hintName = "generatedFile")
{
_content = content;
_hintName = hintName;
}
public void Execute(SourceGeneratorContext context)
{
context.AdditionalSources.Add(this._hintName, SourceText.From(_content, Encoding.UTF8));
}
public void Initialize(InitializationContext context)
{
}
}
internal class CallbackGenerator : ISourceGenerator
{
private readonly Action<InitializationContext> _onInit;
private readonly Action<SourceGeneratorContext> _onExecute;
private readonly string _sourceOpt;
public CallbackGenerator(Action<InitializationContext> onInit, Action<SourceGeneratorContext> onExecute, string sourceOpt = "")
[Fact]
public void Generator_HintName_MustBe_Unique()
{
_onInit = onInit;
_onExecute = onExecute;
_sourceOpt = sourceOpt;
}
var source = @"
class C { }
";
var parseOptions = TestOptions.Regular;
Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions);
compilation.VerifyDiagnostics();
Assert.Single(compilation.SyntaxTrees);
public void Execute(SourceGeneratorContext context)
{
_onExecute(context);
if (!string.IsNullOrWhiteSpace(_sourceOpt))
var generator = new CallbackGenerator((ic) => { }, (sgc) =>
{
context.AdditionalSources.Add("source.cs", SourceText.From(_sourceOpt, Encoding.UTF8));
}
}
public void Initialize(InitializationContext context) => _onInit(context);
}
sgc.AddSource("test", SourceText.From("public class D{}", Encoding.UTF8));
internal class AdditionalFileAddedGenerator : ISourceGenerator
{
public bool CanApplyChanges { get; set; } = true;
// the assert should swallow the exception, so we'll actually succesfully generate
Assert.Throws<ArgumentException>("hintName", () => sgc.AddSource("test", SourceText.From("public class D{}", Encoding.UTF8)));
public void Execute(SourceGeneratorContext context)
{
foreach (var file in context.AdditionalFiles)
{
AddSourceForAdditionalFile(context.AdditionalSources, file);
}
}
// also throws for <name> vs <name>.cs
Assert.Throws<ArgumentException>("hintName", () => sgc.AddSource("test.cs", SourceText.From("public class D{}", Encoding.UTF8)));
});
public void Initialize(InitializationContext context)
{
context.RegisterForAdditionalFileChanges(UpdateContext);
GeneratorDriver driver = new CSharpGeneratorDriver(parseOptions, ImmutableArray.Create<ISourceGenerator>(generator), ImmutableArray<AdditionalText>.Empty);
driver.RunFullGeneration(compilation, out var outputCompilation, out var generatorDiagnostics);
outputCompilation.VerifyDiagnostics();
generatorDiagnostics.Verify();
Assert.Equal(2, outputCompilation.SyntaxTrees.Count());
}
bool UpdateContext(EditContext context, AdditionalFileEdit edit)
[Fact]
public void Generator_HintName_Is_Appended_With_GeneratorName()
{
if (edit is AdditionalFileAddedEdit add && CanApplyChanges)
{
AddSourceForAdditionalFile(context.AdditionalSources, add.AddedText);
return true;
}
return false;
}
private void AddSourceForAdditionalFile(AdditionalSourcesCollection sources, AdditionalText file) => sources.Add(GetGeneratedFileName(file.Path), SourceText.From("", Encoding.UTF8));
var source = @"
class C { }
";
var parseOptions = TestOptions.Regular;
Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions);
compilation.VerifyDiagnostics();
Assert.Single(compilation.SyntaxTrees);
private string GetGeneratedFileName(string path) => $"{Path.GetFileNameWithoutExtension(path)}.generated";
}
var generator = new SingleFileTestGenerator("public class D {}", "source.cs");
var generator2 = new SingleFileTestGenerator2("public class E {}", "source.cs");
internal class InMemoryAdditionalText : AdditionalText
{
private readonly SourceText _content;
GeneratorDriver driver = new CSharpGeneratorDriver(parseOptions, ImmutableArray.Create<ISourceGenerator>(generator, generator2), ImmutableArray<AdditionalText>.Empty);
driver.RunFullGeneration(compilation, out var outputCompilation, out var generatorDiagnostics);
outputCompilation.VerifyDiagnostics();
generatorDiagnostics.Verify();
Assert.Equal(3, outputCompilation.SyntaxTrees.Count());
public InMemoryAdditionalText(string path, string content)
{
Path = path;
_content = SourceText.From(content);
var filePaths = outputCompilation.SyntaxTrees.Skip(1).Select(t => t.FilePath).ToArray();
Assert.Equal(new[] {
$"{generator.GetType().Module.ModuleVersionId}_{generator.GetType().FullName}_source.cs",
$"{generator2.GetType().Module.ModuleVersionId}_{generator2.GetType().FullName}_source.cs"
}, filePaths);
}
public override string Path { get; }
public override SourceText GetText(CancellationToken cancellationToken = default) => _content;
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using System.IO;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.CSharp.Semantic.UnitTests.SourceGeneration
{
internal class SingleFileTestGenerator : ISourceGenerator
{
private readonly string _content;
private readonly string _hintName;
public SingleFileTestGenerator(string content, string hintName = "generatedFile")
{
_content = content;
_hintName = hintName;
}
public void Execute(SourceGeneratorContext context)
{
context.AdditionalSources.Add(this._hintName, SourceText.From(_content, Encoding.UTF8));
}
public void Initialize(InitializationContext context)
{
}
}
internal class SingleFileTestGenerator2 : SingleFileTestGenerator
{
public SingleFileTestGenerator2(string content, string hintName = "generatedFile") : base(content, hintName)
{
}
}
internal class CallbackGenerator : ISourceGenerator
{
private readonly Action<InitializationContext> _onInit;
private readonly Action<SourceGeneratorContext> _onExecute;
private readonly string? _source;
public CallbackGenerator(Action<InitializationContext> onInit, Action<SourceGeneratorContext> onExecute, string? source = "")
{
_onInit = onInit;
_onExecute = onExecute;
_source = source;
}
public void Execute(SourceGeneratorContext context)
{
_onExecute(context);
if (!string.IsNullOrWhiteSpace(_source))
{
context.AdditionalSources.Add("source.cs", SourceText.From(_source, Encoding.UTF8));
}
}
public void Initialize(InitializationContext context) => _onInit(context);
}
internal class CallbackGenerator2 : CallbackGenerator
{
public CallbackGenerator2(Action<InitializationContext> onInit, Action<SourceGeneratorContext> onExecute, string? source = "") : base(onInit, onExecute, source)
{
}
}
internal class AdditionalFileAddedGenerator : ISourceGenerator
{
public bool CanApplyChanges { get; set; } = true;
public void Execute(SourceGeneratorContext context)
{
foreach (var file in context.AdditionalFiles)
{
AddSourceForAdditionalFile(context.AdditionalSources, file);
}
}
public void Initialize(InitializationContext context)
{
context.RegisterForAdditionalFileChanges(UpdateContext);
}
bool UpdateContext(EditContext context, AdditionalFileEdit edit)
{
if (edit is AdditionalFileAddedEdit add && CanApplyChanges)
{
AddSourceForAdditionalFile(context.AdditionalSources, add.AddedText);
return true;
}
return false;
}
private void AddSourceForAdditionalFile(AdditionalSourcesCollection sources, AdditionalText file) => sources.Add(GetGeneratedFileName(file.Path), SourceText.From("", Encoding.UTF8));
private string GetGeneratedFileName(string path) => $"{Path.GetFileNameWithoutExtension(path.Replace('\\', Path.DirectorySeparatorChar))}.generated";
}
internal class InMemoryAdditionalText : AdditionalText
{
private readonly SourceText _content;
public InMemoryAdditionalText(string path, string content)
{
Path = path;
_content = SourceText.From(content, Encoding.UTF8);
}
public override string Path { get; }
public override SourceText GetText(CancellationToken cancellationToken = default) => _content;
}
}
......@@ -689,6 +689,7 @@
</data>
<data name="Single_type_per_generator_0" xml:space="preserve">
<value>Only a single {0} can be registered per generator.</value>
<comment>{0}: type name</comment>
</data>
<data name="WRN_MultipleGlobalAnalyzerKeys" xml:space="preserve">
<value>Multiple global analyzer config files set the same key '{0}' in section '{1}'. It has been unset. Key was set by the following files: '{2}'</value>
......@@ -696,4 +697,11 @@
<data name="WRN_MultipleGlobalAnalyzerKeys_Title" xml:space="preserve">
<value>Multiple global analyzer config files set the same key. It has been unset.</value>
</data>
<data name="HintNameUniquePerGenerator" xml:space="preserve">
<value>The hintName of the added source file must be unique within a generator.</value>
</data>
<data name="HintNameInvalidChar" xml:space="preserve">
<value>The hintName contains an invalid character '{0}' at position {1}.</value>
<comment>{0}: the invalid character, {1} the position it occured at</comment>
</data>
</root>
\ No newline at end of file
......@@ -791,13 +791,15 @@ private int RunCore(TextWriter consoleOutput, ErrorLogger errorLogger, Cancellat
// We pass it to the generators, which will realize any symbols they require.
compilation = RunGenerators(compilation, Arguments.ParseOptions, generators, additionalTexts, diagnostics);
// https://github.com/dotnet/roslyn/issues/44087
// Workaround by getting options for any generated trees that were produced.
// In the future we'll want to apply the config set rules at parse time, return the options, and add them in here
if (!sourceFileAnalyzerConfigOptions.IsDefault && generators.Length > 0)
if (generators.Length > 0)
{
var generatedOptions = compilation.SyntaxTrees.Skip(sourceFileAnalyzerConfigOptions.Length).Select(f => analyzerConfigSet.GetOptionsForSourcePath(f.FilePath));
sourceFileAnalyzerConfigOptions = sourceFileAnalyzerConfigOptions.AddRange(generatedOptions);
var generatedSyntaxTrees = compilation.SyntaxTrees.Skip(Arguments.SourceFiles.Length);
if (!sourceFileAnalyzerConfigOptions.IsDefault)
{
sourceFileAnalyzerConfigOptions = sourceFileAnalyzerConfigOptions.AddRange(generatedSyntaxTrees.Select(f => analyzerConfigSet.GetOptionsForSourcePath(f.FilePath)));
}
embeddedTexts = embeddedTexts.AddRange(generatedSyntaxTrees.Select(t => EmbeddedText.FromSource(t.FilePath, t.GetText())));
}
CompileAndEmit(
......
......@@ -3,7 +3,9 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
......@@ -13,44 +15,96 @@ namespace Microsoft.CodeAnalysis
{
internal sealed class AdditionalSourcesCollection
{
private readonly PooledDictionary<string, SourceText> _sourcesAdded;
private readonly ArrayBuilder<GeneratedSourceText> _sourcesAdded;
private const StringComparison _hintNameComparison = StringComparison.OrdinalIgnoreCase;
private static readonly StringComparer s_hintNameComparer = StringComparer.OrdinalIgnoreCase;
internal AdditionalSourcesCollection()
{
_sourcesAdded = PooledDictionary<string, SourceText>.GetInstance();
_sourcesAdded = ArrayBuilder<GeneratedSourceText>.GetInstance();
}
internal AdditionalSourcesCollection(ImmutableArray<GeneratedSourceText> sources)
internal AdditionalSourcesCollection(ImmutableArray<GeneratedSourceText> existingSources)
: this()
{
foreach (var source in sources)
{
_sourcesAdded.Add(source.HintName, source.Text);
}
_sourcesAdded.AddRange(existingSources);
}
public void Add(string hintName, SourceText source)
{
_sourcesAdded.Add(hintName, source);
if (string.IsNullOrWhiteSpace(hintName))
{
throw new ArgumentNullException(nameof(hintName));
}
// allow any identifier character or [.,-_ ()[]{}]
for (int i = 0; i < hintName.Length; i++)
{
char c = hintName[i];
if (!UnicodeCharacterUtilities.IsIdentifierPartCharacter(c)
&& c != '.'
&& c != ','
&& c != '-'
&& c != '_'
&& c != ' '
&& c != '('
&& c != ')'
&& c != '['
&& c != ']'
&& c != '{'
&& c != '}')
{
throw new ArgumentException(string.Format(CodeAnalysisResources.HintNameInvalidChar, c, i), nameof(hintName));
}
}
hintName = AppendExtensionIfRequired(hintName);
if (this.Contains(hintName))
{
throw new ArgumentException(CodeAnalysisResources.HintNameUniquePerGenerator, nameof(hintName));
}
_sourcesAdded.Add(new GeneratedSourceText(hintName, source));
}
public void RemoveSource(string hintName)
{
_sourcesAdded.Remove(hintName);
hintName = AppendExtensionIfRequired(hintName);
for (int i = 0; i < _sourcesAdded.Count; i++)
{
if (s_hintNameComparer.Equals(_sourcesAdded[i].HintName, hintName))
{
_sourcesAdded.RemoveAt(i);
return;
}
}
}
public bool Contains(string hintName)
{
hintName = AppendExtensionIfRequired(hintName);
for (int i = 0; i < _sourcesAdded.Count; i++)
{
if (s_hintNameComparer.Equals(_sourcesAdded[i].HintName, hintName))
{
return true;
}
}
return false;
}
public bool Contains(string hintName) => _sourcesAdded.ContainsKey(hintName);
internal ImmutableArray<GeneratedSourceText> ToImmutableAndFree() => _sourcesAdded.ToImmutableAndFree();
internal ImmutableArray<GeneratedSourceText> ToImmutableAndFree()
private static string AppendExtensionIfRequired(string hintName)
{
// https://github.com/dotnet/roslyn/issues/42627: This needs to be consistently ordered
ArrayBuilder<GeneratedSourceText> builder = ArrayBuilder<GeneratedSourceText>.GetInstance();
foreach (var (hintName, sourceText) in _sourcesAdded)
if (!hintName.EndsWith(".cs", _hintNameComparison))
{
builder.Add(new GeneratedSourceText(hintName, sourceText));
hintName = string.Concat(hintName, ".cs");
}
_sourcesAdded.Free();
return builder.ToImmutableAndFree();
return hintName;
}
}
}
......@@ -13,8 +13,7 @@ namespace Microsoft.CodeAnalysis
/// <summary>
/// A source text created by an <see cref="ISourceGenerator"/>
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0016:Add public types and members to the declared API", Justification = "In Progress")]
public readonly struct GeneratedSourceText
internal readonly struct GeneratedSourceText
{
public SourceText Text { get; }
......
......@@ -4,6 +4,8 @@
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.PooledObjects;
......@@ -35,12 +37,13 @@ public abstract class GeneratorDriver
internal GeneratorDriver(GeneratorDriverState state)
{
Debug.Assert(state.Generators.GroupBy(s => s.GetType()).Count() == state.Generators.Length); // ensure we don't have duplicate generator types
_state = state;
}
internal GeneratorDriver(ParseOptions parseOptions, ImmutableArray<ISourceGenerator> generators, ImmutableArray<AdditionalText> additionalTexts)
{
_state = new GeneratorDriverState(parseOptions, generators, additionalTexts, ImmutableDictionary<ISourceGenerator, GeneratorState>.Empty, ImmutableArray<PendingEdit>.Empty, editsFailed: true);
_state = new GeneratorDriverState(parseOptions, generators, additionalTexts, ImmutableArray.Create(new GeneratorState[generators.Length]), ImmutableArray<PendingEdit>.Empty, editsFailed: true);
}
public GeneratorDriver RunFullGeneration(Compilation compilation, out Compilation outputCompilation, out ImmutableArray<Diagnostic> diagnostics, CancellationToken cancellationToken = default)
......@@ -55,22 +58,21 @@ public GeneratorDriver RunFullGeneration(Compilation compilation, out Compilatio
// run the actual generation
var state = StateWithPendingEditsApplied(_state);
var stateBuilder = PooledDictionary<ISourceGenerator, GeneratorState>.GetInstance();
var stateBuilder = ArrayBuilder<GeneratorState>.GetInstance();
var receivers = PooledDictionary<ISourceGenerator, ISyntaxReceiver>.GetInstance();
var diagnosticsBag = new DiagnosticBag();
foreach (var generator in state.Generators)
for (int i = 0; i < state.Generators.Length; i++)
{
var generator = state.Generators[i];
var generatorState = state.GeneratorStates[i];
// initialize the generator if needed
if (!state.GeneratorStates.TryGetValue(generator, out GeneratorState generatorState))
if (!generatorState.Info.Initialized)
{
generatorState = InitializeGenerator(generator, diagnosticsBag, cancellationToken);
}
if (generatorState.Info.Initialized)
{
stateBuilder.Add(generator, generatorState);
}
stateBuilder.Add(generatorState);
// create the syntax receiver if requested
if (generatorState.Info.SyntaxReceiverCreator is object)
......@@ -91,22 +93,30 @@ public GeneratorDriver RunFullGeneration(Compilation compilation, out Compilatio
}
// https://github.com/dotnet/roslyn/issues/42629: should be possible to parallelize this
foreach (var (generator, generatorState) in stateBuilder.ToImmutableArray())
for (int i = 0; i < state.Generators.Length; i++)
{
var generator = state.Generators[i];
var generatorState = stateBuilder[i];
try
{
// don't try and generate if initialization failed
if (!generatorState.Info.Initialized)
{
continue;
}
// we create a new context for each run of the generator. We'll never re-use existing state, only replace anything we have
_ = receivers.TryGetValue(generator, out var syntaxReceiverOpt);
var context = new SourceGeneratorContext(compilation, state.AdditionalTexts.NullToEmpty(), syntaxReceiverOpt, diagnosticsBag);
generator.Execute(context);
stateBuilder[generator] = generatorState.WithSources(ParseAdditionalSources(context.AdditionalSources.ToImmutableAndFree(), cancellationToken));
stateBuilder[i] = generatorState.WithSources(ParseAdditionalSources(generator, context.AdditionalSources.ToImmutableAndFree(), cancellationToken));
}
catch
{
diagnosticsBag.Add(Diagnostic.Create(MessageProvider, MessageProvider.WRN_GeneratorFailedDuringGeneration, generator.GetType().Name));
}
}
state = state.With(generatorStates: stateBuilder.ToImmutableDictionaryAndFree());
state = state.With(generatorStates: stateBuilder.ToImmutableAndFree());
diagnostics = diagnosticsBag.ToReadOnlyAndFree();
// build the final state, and return
......@@ -146,14 +156,25 @@ public GeneratorDriver TryApplyEdits(Compilation compilation, out Compilation ou
public GeneratorDriver AddGenerators(ImmutableArray<ISourceGenerator> generators)
{
// set editsFailed true, as we won't be able to apply edits with a new generator
var newState = _state.With(generators: _state.Generators.AddRange(generators), editsFailed: true);
var newState = _state.With(generators: _state.Generators.AddRange(generators), generatorStates: _state.GeneratorStates.AddRange(new GeneratorState[generators.Length]), editsFailed: true);
return FromState(newState);
}
public GeneratorDriver RemoveGenerators(ImmutableArray<ISourceGenerator> generators)
{
var newState = _state.With(generators: _state.Generators.RemoveRange(generators), generatorStates: _state.GeneratorStates.RemoveRange(generators));
return FromState(newState);
var newGenerators = _state.Generators;
var newStates = _state.GeneratorStates;
for (int i = 0; i < newGenerators.Length; i++)
{
if (generators.Contains(newGenerators[i]))
{
newGenerators = newGenerators.RemoveAt(i);
newStates = newStates.RemoveAt(i);
i--;
}
}
return FromState(_state.With(generators: newGenerators, generatorStates: newStates));
}
public GeneratorDriver AddAdditionalTexts(ImmutableArray<AdditionalText> additionalTexts)
......@@ -175,8 +196,10 @@ private GeneratorDriverState ApplyPartialEdit(GeneratorDriverState state, Pendin
// see if any generators accept this particular edit
var stateBuilder = PooledDictionary<ISourceGenerator, GeneratorState>.GetInstance();
foreach (var (generator, generatorState) in state.GeneratorStates)
for (int i = 0; i < initialState.Generators.Length; i++)
{
var generator = initialState.Generators[i];
var generatorState = initialState.GeneratorStates[i];
if (edit.AcceptedBy(generatorState.Info))
{
// attempt to apply the edit
......@@ -190,7 +213,7 @@ private GeneratorDriverState ApplyPartialEdit(GeneratorDriverState state, Pendin
}
// update the state with the new edits
state = state.With(generatorStates: state.GeneratorStates.SetItem(generator, generatorState.WithSources(ParseAdditionalSources(context.AdditionalSources.ToImmutableAndFree(), cancellationToken))));
state = state.With(generatorStates: state.GeneratorStates.SetItem(i, generatorState.WithSources(ParseAdditionalSources(generator, context.AdditionalSources.ToImmutableAndFree(), cancellationToken))));
}
}
state = edit.Commit(state);
......@@ -230,7 +253,7 @@ private GeneratorState InitializeGenerator(ISourceGenerator generator, Diagnosti
private static Compilation RemoveGeneratedSyntaxTrees(GeneratorDriverState state, Compilation compilation)
{
ArrayBuilder<SyntaxTree> trees = ArrayBuilder<SyntaxTree>.GetInstance();
foreach (var (_, generatorState) in state.GeneratorStates)
foreach (var generatorState in state.GeneratorStates)
{
foreach (var (_, tree) in generatorState.Sources)
{
......@@ -246,12 +269,14 @@ private static Compilation RemoveGeneratedSyntaxTrees(GeneratorDriverState state
return comp;
}
private ImmutableDictionary<GeneratedSourceText, SyntaxTree> ParseAdditionalSources(ImmutableArray<GeneratedSourceText> generatedSources, CancellationToken cancellationToken)
private ImmutableDictionary<GeneratedSourceText, SyntaxTree> ParseAdditionalSources(ISourceGenerator generator, ImmutableArray<GeneratedSourceText> generatedSources, CancellationToken cancellationToken)
{
var trees = PooledDictionary<GeneratedSourceText, SyntaxTree>.GetInstance();
var type = generator.GetType();
var prefix = $"{type.Module.ModuleVersionId}_{type.FullName}";
foreach (var source in generatedSources)
{
trees.Add(source, ParseGeneratedSourceText(source, cancellationToken));
trees.Add(source, ParseGeneratedSourceText(source, $"{prefix}_{source.HintName}", cancellationToken));
}
return trees.ToImmutableDictionaryAndFree();
}
......@@ -259,7 +284,7 @@ private static Compilation RemoveGeneratedSyntaxTrees(GeneratorDriverState state
private GeneratorDriver BuildFinalCompilation(Compilation compilation, out Compilation outputCompilation, GeneratorDriverState state, CancellationToken cancellationToken)
{
ArrayBuilder<SyntaxTree> trees = ArrayBuilder<SyntaxTree>.GetInstance();
foreach (var (generator, generatorState) in state.GeneratorStates)
foreach (var generatorState in state.GeneratorStates)
{
trees.AddRange(generatorState.Sources.Values);
}
......@@ -275,6 +300,6 @@ private GeneratorDriver BuildFinalCompilation(Compilation compilation, out Compi
internal abstract GeneratorDriver FromState(GeneratorDriverState state);
internal abstract SyntaxTree ParseGeneratedSourceText(GeneratedSourceText input, CancellationToken cancellationToken);
internal abstract SyntaxTree ParseGeneratedSourceText(GeneratedSourceText input, string fileName, CancellationToken cancellationToken);
}
}
......@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text;
#nullable enable
......@@ -15,7 +16,7 @@ namespace Microsoft.CodeAnalysis
internal GeneratorDriverState(ParseOptions parseOptions,
ImmutableArray<ISourceGenerator> generators,
ImmutableArray<AdditionalText> additionalTexts,
ImmutableDictionary<ISourceGenerator, GeneratorState> generatorStates,
ImmutableArray<GeneratorState> generatorStates,
ImmutableArray<PendingEdit> edits,
bool editsFailed)
{
......@@ -25,6 +26,8 @@ namespace Microsoft.CodeAnalysis
Edits = edits;
ParseOptions = parseOptions;
EditsFailed = editsFailed;
Debug.Assert(Generators.Length == GeneratorStates.Length);
}
/// <summary>
......@@ -40,10 +43,10 @@ namespace Microsoft.CodeAnalysis
/// The last run state of each generator, by the generator that created it
/// </summary>
/// <remarks>
/// If the driver this state belongs to has yet to perform generation, this will be empty.
/// After generation there *should* be a 1-to-1 mapping for each generator, unless that generator failed to initialize.
/// There will be a 1-to-1 mapping for each generator. If a generator has yet to
/// be initialized or failed during initialization it's state will be <c>default(GeneratorState)</c>
/// </remarks>
internal readonly ImmutableDictionary<ISourceGenerator, GeneratorState> GeneratorStates;
internal readonly ImmutableArray<GeneratorState> GeneratorStates;
/// <summary>
/// The set of <see cref="AdditionalText"/>s available to source generators during a run
......@@ -68,7 +71,7 @@ namespace Microsoft.CodeAnalysis
internal GeneratorDriverState With(
ParseOptions? parseOptions = null,
ImmutableArray<ISourceGenerator>? generators = null,
ImmutableDictionary<ISourceGenerator, GeneratorState>? generatorStates = null,
ImmutableArray<GeneratorState>? generatorStates = null,
ImmutableArray<AdditionalText>? additionalTexts = null,
ImmutableArray<PendingEdit>? edits = null,
bool? editsFailed = null)
......
......@@ -3,14 +3,9 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
#nullable enable
namespace Microsoft.CodeAnalysis
......@@ -59,8 +54,20 @@ internal SourceGeneratorContext(Compilation compilation, ImmutableArray<Addition
internal AdditionalSourcesCollection AdditionalSources { get; }
/// <summary>
/// Adds a <see cref="SourceText"/> to the compilation
/// </summary>
/// <param name="hintName">An identifier that can be used to reference this source text, must be unique within this generator</param>
/// <param name="sourceText">The <see cref="SourceText"/> to add to the compilation</param>
public void AddSource(string hintName, SourceText sourceText) => AdditionalSources.Add(hintName, sourceText);
/// <summary>
/// Adds a <see cref="Diagnostic"/> to the users compilation
/// </summary>
/// <param name="diagnostic">The diagnostic that should be added to the compilation</param>
/// <remarks>
/// The severity of the diagnostic may cause the compilation to fail, depending on the <see cref="Compilation"/> settings.
/// </remarks>
public void ReportDiagnostic(Diagnostic diagnostic) => _diagnostics.Add(diagnostic);
}
......
......@@ -29,6 +29,16 @@
<target state="translated">Pokud chcete tento analyzátor zakázat, potlačte následující diagnostiku: {0}</target>
<note>{0}: Comma-separated list of diagnostic IDs</note>
</trans-unit>
<trans-unit id="HintNameInvalidChar">
<source>The hintName contains an invalid character '{0}' at position {1}.</source>
<target state="new">The hintName contains an invalid character '{0}' at position {1}.</target>
<note>{0}: the invalid character, {1} the position it occured at</note>
</trans-unit>
<trans-unit id="HintNameUniquePerGenerator">
<source>The hintName of the added source file must be unique within a generator.</source>
<target state="new">The hintName of the added source file must be unique within a generator.</target>
<note />
</trans-unit>
<trans-unit id="InvalidDiagnosticSuppressionReported">
<source>Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor.</source>
<target state="translated">Potlačené ID diagnostiky {0} neodpovídá potlačitelnému ID {1} pro daný popisovač potlačení.</target>
......@@ -96,8 +106,8 @@
</trans-unit>
<trans-unit id="Single_type_per_generator_0">
<source>Only a single {0} can be registered per generator.</source>
<target state="translated">Na jeden generátor je možné zaregistrovat jen jedno {0}.</target>
<note />
<target state="needs-review-translation">Na jeden generátor je možné zaregistrovat jen jedno {0}.</target>
<note>{0}: type name</note>
</trans-unit>
<trans-unit id="SupportedDiagnosticsHasNullDescriptor">
<source>Analyzer '{0}' contains a null descriptor in its 'SupportedDiagnostics'.</source>
......
......@@ -29,6 +29,16 @@
<target state="translated">Unterdrücken Sie die folgende Diagnose, um dieses Analysetool zu deaktivieren: {0}</target>
<note>{0}: Comma-separated list of diagnostic IDs</note>
</trans-unit>
<trans-unit id="HintNameInvalidChar">
<source>The hintName contains an invalid character '{0}' at position {1}.</source>
<target state="new">The hintName contains an invalid character '{0}' at position {1}.</target>
<note>{0}: the invalid character, {1} the position it occured at</note>
</trans-unit>
<trans-unit id="HintNameUniquePerGenerator">
<source>The hintName of the added source file must be unique within a generator.</source>
<target state="new">The hintName of the added source file must be unique within a generator.</target>
<note />
</trans-unit>
<trans-unit id="InvalidDiagnosticSuppressionReported">
<source>Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor.</source>
<target state="translated">Die unterdrückte Diagnose-ID "{0}" entspricht nicht der unterdrückbaren ID "{1}" für den angegebenen Deskriptor zur Unterdrückung.</target>
......@@ -96,8 +106,8 @@
</trans-unit>
<trans-unit id="Single_type_per_generator_0">
<source>Only a single {0} can be registered per generator.</source>
<target state="translated">Pro Generator kann nur ein einzelner {0} registriert werden.</target>
<note />
<target state="needs-review-translation">Pro Generator kann nur ein einzelner {0} registriert werden.</target>
<note>{0}: type name</note>
</trans-unit>
<trans-unit id="SupportedDiagnosticsHasNullDescriptor">
<source>Analyzer '{0}' contains a null descriptor in its 'SupportedDiagnostics'.</source>
......
......@@ -29,6 +29,16 @@
<target state="translated">Suprima el diagnóstico siguiente para deshabilitar este analizador: {0}</target>
<note>{0}: Comma-separated list of diagnostic IDs</note>
</trans-unit>
<trans-unit id="HintNameInvalidChar">
<source>The hintName contains an invalid character '{0}' at position {1}.</source>
<target state="new">The hintName contains an invalid character '{0}' at position {1}.</target>
<note>{0}: the invalid character, {1} the position it occured at</note>
</trans-unit>
<trans-unit id="HintNameUniquePerGenerator">
<source>The hintName of the added source file must be unique within a generator.</source>
<target state="new">The hintName of the added source file must be unique within a generator.</target>
<note />
</trans-unit>
<trans-unit id="InvalidDiagnosticSuppressionReported">
<source>Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor.</source>
<target state="translated">El id. de diagnóstico "{0}" suprimido no coincide con el id. "{1}" que se puede suprimir para el descriptor de supresión dado.</target>
......@@ -96,8 +106,8 @@
</trans-unit>
<trans-unit id="Single_type_per_generator_0">
<source>Only a single {0} can be registered per generator.</source>
<target state="translated">Solo puede registrarse un único tipo de {0} por generador.</target>
<note />
<target state="needs-review-translation">Solo puede registrarse un único tipo de {0} por generador.</target>
<note>{0}: type name</note>
</trans-unit>
<trans-unit id="SupportedDiagnosticsHasNullDescriptor">
<source>Analyzer '{0}' contains a null descriptor in its 'SupportedDiagnostics'.</source>
......
......@@ -29,6 +29,16 @@
<target state="translated">Supprimez les diagnostics suivants pour désactiver cet analyseur : {0}</target>
<note>{0}: Comma-separated list of diagnostic IDs</note>
</trans-unit>
<trans-unit id="HintNameInvalidChar">
<source>The hintName contains an invalid character '{0}' at position {1}.</source>
<target state="new">The hintName contains an invalid character '{0}' at position {1}.</target>
<note>{0}: the invalid character, {1} the position it occured at</note>
</trans-unit>
<trans-unit id="HintNameUniquePerGenerator">
<source>The hintName of the added source file must be unique within a generator.</source>
<target state="new">The hintName of the added source file must be unique within a generator.</target>
<note />
</trans-unit>
<trans-unit id="InvalidDiagnosticSuppressionReported">
<source>Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor.</source>
<target state="translated">L'ID de diagnostic supprimé '{0}' ne correspond pas à l'ID supprimable '{1}' pour le descripteur de suppression spécifié.</target>
......@@ -96,8 +106,8 @@
</trans-unit>
<trans-unit id="Single_type_per_generator_0">
<source>Only a single {0} can be registered per generator.</source>
<target state="translated">Un seul {0} peut être inscrit par générateur.</target>
<note />
<target state="needs-review-translation">Un seul {0} peut être inscrit par générateur.</target>
<note>{0}: type name</note>
</trans-unit>
<trans-unit id="SupportedDiagnosticsHasNullDescriptor">
<source>Analyzer '{0}' contains a null descriptor in its 'SupportedDiagnostics'.</source>
......
......@@ -29,6 +29,16 @@
<target state="translated">Per disabilitare questo analizzatore, eliminare la diagnostica seguente: {0}</target>
<note>{0}: Comma-separated list of diagnostic IDs</note>
</trans-unit>
<trans-unit id="HintNameInvalidChar">
<source>The hintName contains an invalid character '{0}' at position {1}.</source>
<target state="new">The hintName contains an invalid character '{0}' at position {1}.</target>
<note>{0}: the invalid character, {1} the position it occured at</note>
</trans-unit>
<trans-unit id="HintNameUniquePerGenerator">
<source>The hintName of the added source file must be unique within a generator.</source>
<target state="new">The hintName of the added source file must be unique within a generator.</target>
<note />
</trans-unit>
<trans-unit id="InvalidDiagnosticSuppressionReported">
<source>Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor.</source>
<target state="translated">L'ID diagnostica '{0}' eliminato non corrisponde all'ID eliminabile '{1}' per il descrittore di eliminazione specificato.</target>
......@@ -96,8 +106,8 @@
</trans-unit>
<trans-unit id="Single_type_per_generator_0">
<source>Only a single {0} can be registered per generator.</source>
<target state="translated">È possibile registrare solo un tipo {0} per generatore.</target>
<note />
<target state="needs-review-translation">È possibile registrare solo un tipo {0} per generatore.</target>
<note>{0}: type name</note>
</trans-unit>
<trans-unit id="SupportedDiagnosticsHasNullDescriptor">
<source>Analyzer '{0}' contains a null descriptor in its 'SupportedDiagnostics'.</source>
......
......@@ -29,6 +29,16 @@
<target state="translated">次の診断を抑制して、このアナライザーを無効にします: {0}</target>
<note>{0}: Comma-separated list of diagnostic IDs</note>
</trans-unit>
<trans-unit id="HintNameInvalidChar">
<source>The hintName contains an invalid character '{0}' at position {1}.</source>
<target state="new">The hintName contains an invalid character '{0}' at position {1}.</target>
<note>{0}: the invalid character, {1} the position it occured at</note>
</trans-unit>
<trans-unit id="HintNameUniquePerGenerator">
<source>The hintName of the added source file must be unique within a generator.</source>
<target state="new">The hintName of the added source file must be unique within a generator.</target>
<note />
</trans-unit>
<trans-unit id="InvalidDiagnosticSuppressionReported">
<source>Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor.</source>
<target state="translated">抑制された診断 ID '{0}' が、指定された抑制記述子の抑制可能な ID '{1}' と一致しません。</target>
......@@ -96,8 +106,8 @@
</trans-unit>
<trans-unit id="Single_type_per_generator_0">
<source>Only a single {0} can be registered per generator.</source>
<target state="translated">ジェネレーターごとに登録できるのは 1 つの {0} のみです。</target>
<note />
<target state="needs-review-translation">ジェネレーターごとに登録できるのは 1 つの {0} のみです。</target>
<note>{0}: type name</note>
</trans-unit>
<trans-unit id="SupportedDiagnosticsHasNullDescriptor">
<source>Analyzer '{0}' contains a null descriptor in its 'SupportedDiagnostics'.</source>
......
......@@ -29,6 +29,16 @@
<target state="translated">이 분석기를 사용하지 않도록 설정하려면 다음 진단이 표시되지 않도록 설정하세요. {0}</target>
<note>{0}: Comma-separated list of diagnostic IDs</note>
</trans-unit>
<trans-unit id="HintNameInvalidChar">
<source>The hintName contains an invalid character '{0}' at position {1}.</source>
<target state="new">The hintName contains an invalid character '{0}' at position {1}.</target>
<note>{0}: the invalid character, {1} the position it occured at</note>
</trans-unit>
<trans-unit id="HintNameUniquePerGenerator">
<source>The hintName of the added source file must be unique within a generator.</source>
<target state="new">The hintName of the added source file must be unique within a generator.</target>
<note />
</trans-unit>
<trans-unit id="InvalidDiagnosticSuppressionReported">
<source>Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor.</source>
<target state="translated">표시되지 않는 진단 ID '{0}'이(가) 지정된 비표시 설명자의 표시하지 않을 수 있는 ID '{1}'과(와) 일치하지 않습니다.</target>
......@@ -96,8 +106,8 @@
</trans-unit>
<trans-unit id="Single_type_per_generator_0">
<source>Only a single {0} can be registered per generator.</source>
<target state="translated">생성기당 하나의 {0}만 등록할 수 있습니다.</target>
<note />
<target state="needs-review-translation">생성기당 하나의 {0}만 등록할 수 있습니다.</target>
<note>{0}: type name</note>
</trans-unit>
<trans-unit id="SupportedDiagnosticsHasNullDescriptor">
<source>Analyzer '{0}' contains a null descriptor in its 'SupportedDiagnostics'.</source>
......
......@@ -29,6 +29,16 @@
<target state="translated">Pomiń następującą diagnostykę, aby wyłączyć ten analizator: {0}</target>
<note>{0}: Comma-separated list of diagnostic IDs</note>
</trans-unit>
<trans-unit id="HintNameInvalidChar">
<source>The hintName contains an invalid character '{0}' at position {1}.</source>
<target state="new">The hintName contains an invalid character '{0}' at position {1}.</target>
<note>{0}: the invalid character, {1} the position it occured at</note>
</trans-unit>
<trans-unit id="HintNameUniquePerGenerator">
<source>The hintName of the added source file must be unique within a generator.</source>
<target state="new">The hintName of the added source file must be unique within a generator.</target>
<note />
</trans-unit>
<trans-unit id="InvalidDiagnosticSuppressionReported">
<source>Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor.</source>
<target state="translated">Pominięty identyfikator diagnostyki „{0}” nie pasuje do możliwego do pominięcia identyfikatora „{1}” dla danego deskryptora pomijania.</target>
......@@ -96,8 +106,8 @@
</trans-unit>
<trans-unit id="Single_type_per_generator_0">
<source>Only a single {0} can be registered per generator.</source>
<target state="translated">Można zarejestrować tylko jeden element {0} na generator.</target>
<note />
<target state="needs-review-translation">Można zarejestrować tylko jeden element {0} na generator.</target>
<note>{0}: type name</note>
</trans-unit>
<trans-unit id="SupportedDiagnosticsHasNullDescriptor">
<source>Analyzer '{0}' contains a null descriptor in its 'SupportedDiagnostics'.</source>
......
......@@ -29,6 +29,16 @@
<target state="translated">Suprimir os seguintes diagnósticos para desabilitar este analisador: {0}</target>
<note>{0}: Comma-separated list of diagnostic IDs</note>
</trans-unit>
<trans-unit id="HintNameInvalidChar">
<source>The hintName contains an invalid character '{0}' at position {1}.</source>
<target state="new">The hintName contains an invalid character '{0}' at position {1}.</target>
<note>{0}: the invalid character, {1} the position it occured at</note>
</trans-unit>
<trans-unit id="HintNameUniquePerGenerator">
<source>The hintName of the added source file must be unique within a generator.</source>
<target state="new">The hintName of the added source file must be unique within a generator.</target>
<note />
</trans-unit>
<trans-unit id="InvalidDiagnosticSuppressionReported">
<source>Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor.</source>
<target state="translated">A ID de diagnóstico suprimida '{0}' não corresponde à ID suprimível '{1}' para o descritor de supressão fornecido.</target>
......@@ -96,8 +106,8 @@
</trans-unit>
<trans-unit id="Single_type_per_generator_0">
<source>Only a single {0} can be registered per generator.</source>
<target state="translated">Somente um único {0} pode ser registrado por gerador.</target>
<note />
<target state="needs-review-translation">Somente um único {0} pode ser registrado por gerador.</target>
<note>{0}: type name</note>
</trans-unit>
<trans-unit id="SupportedDiagnosticsHasNullDescriptor">
<source>Analyzer '{0}' contains a null descriptor in its 'SupportedDiagnostics'.</source>
......
......@@ -29,6 +29,16 @@
<target state="translated">Отключите следующую диагностику, чтобы отключить этот анализатор: {0}</target>
<note>{0}: Comma-separated list of diagnostic IDs</note>
</trans-unit>
<trans-unit id="HintNameInvalidChar">
<source>The hintName contains an invalid character '{0}' at position {1}.</source>
<target state="new">The hintName contains an invalid character '{0}' at position {1}.</target>
<note>{0}: the invalid character, {1} the position it occured at</note>
</trans-unit>
<trans-unit id="HintNameUniquePerGenerator">
<source>The hintName of the added source file must be unique within a generator.</source>
<target state="new">The hintName of the added source file must be unique within a generator.</target>
<note />
</trans-unit>
<trans-unit id="InvalidDiagnosticSuppressionReported">
<source>Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor.</source>
<target state="translated">Подавленный идентификатор диагностики "{0}" не соответствует подавленному идентификатору "{1}" для заданного дескриптора подавления.</target>
......@@ -96,8 +106,8 @@
</trans-unit>
<trans-unit id="Single_type_per_generator_0">
<source>Only a single {0} can be registered per generator.</source>
<target state="translated">Для каждого генератора можно зарегистрировать только один {0}.</target>
<note />
<target state="needs-review-translation">Для каждого генератора можно зарегистрировать только один {0}.</target>
<note>{0}: type name</note>
</trans-unit>
<trans-unit id="SupportedDiagnosticsHasNullDescriptor">
<source>Analyzer '{0}' contains a null descriptor in its 'SupportedDiagnostics'.</source>
......
......@@ -29,6 +29,16 @@
<target state="translated">Bu çözümleyiciyi devre dışı bırakmak için şu tanılamaları gizleyin: {0}</target>
<note>{0}: Comma-separated list of diagnostic IDs</note>
</trans-unit>
<trans-unit id="HintNameInvalidChar">
<source>The hintName contains an invalid character '{0}' at position {1}.</source>
<target state="new">The hintName contains an invalid character '{0}' at position {1}.</target>
<note>{0}: the invalid character, {1} the position it occured at</note>
</trans-unit>
<trans-unit id="HintNameUniquePerGenerator">
<source>The hintName of the added source file must be unique within a generator.</source>
<target state="new">The hintName of the added source file must be unique within a generator.</target>
<note />
</trans-unit>
<trans-unit id="InvalidDiagnosticSuppressionReported">
<source>Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor.</source>
<target state="translated">'{0}' gizlenmiş tanılama kimliği, belirtilen gizleme tanımlayıcısı için gizlenebilir '{1}' kimliği ile eşleşmiyor.</target>
......@@ -96,8 +106,8 @@
</trans-unit>
<trans-unit id="Single_type_per_generator_0">
<source>Only a single {0} can be registered per generator.</source>
<target state="translated">Oluşturucu başına tek bir {0} kaydedilebilir.</target>
<note />
<target state="needs-review-translation">Oluşturucu başına tek bir {0} kaydedilebilir.</target>
<note>{0}: type name</note>
</trans-unit>
<trans-unit id="SupportedDiagnosticsHasNullDescriptor">
<source>Analyzer '{0}' contains a null descriptor in its 'SupportedDiagnostics'.</source>
......
......@@ -29,6 +29,16 @@
<target state="translated">取消以下诊断以禁用此分析器: {0}</target>
<note>{0}: Comma-separated list of diagnostic IDs</note>
</trans-unit>
<trans-unit id="HintNameInvalidChar">
<source>The hintName contains an invalid character '{0}' at position {1}.</source>
<target state="new">The hintName contains an invalid character '{0}' at position {1}.</target>
<note>{0}: the invalid character, {1} the position it occured at</note>
</trans-unit>
<trans-unit id="HintNameUniquePerGenerator">
<source>The hintName of the added source file must be unique within a generator.</source>
<target state="new">The hintName of the added source file must be unique within a generator.</target>
<note />
</trans-unit>
<trans-unit id="InvalidDiagnosticSuppressionReported">
<source>Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor.</source>
<target state="translated">对于给定的禁止显示描述符,禁止显示的诊断 ID“{0}”与可禁止显示的 ID“{1}”不匹配。</target>
......@@ -96,8 +106,8 @@
</trans-unit>
<trans-unit id="Single_type_per_generator_0">
<source>Only a single {0} can be registered per generator.</source>
<target state="translated">每个生成器仅可注册一个 {0}。</target>
<note />
<target state="needs-review-translation">每个生成器仅可注册一个 {0}。</target>
<note>{0}: type name</note>
</trans-unit>
<trans-unit id="SupportedDiagnosticsHasNullDescriptor">
<source>Analyzer '{0}' contains a null descriptor in its 'SupportedDiagnostics'.</source>
......
......@@ -29,6 +29,16 @@
<target state="translated">請隱藏下列診斷以停用此分析器: {0}</target>
<note>{0}: Comma-separated list of diagnostic IDs</note>
</trans-unit>
<trans-unit id="HintNameInvalidChar">
<source>The hintName contains an invalid character '{0}' at position {1}.</source>
<target state="new">The hintName contains an invalid character '{0}' at position {1}.</target>
<note>{0}: the invalid character, {1} the position it occured at</note>
</trans-unit>
<trans-unit id="HintNameUniquePerGenerator">
<source>The hintName of the added source file must be unique within a generator.</source>
<target state="new">The hintName of the added source file must be unique within a generator.</target>
<note />
</trans-unit>
<trans-unit id="InvalidDiagnosticSuppressionReported">
<source>Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor.</source>
<target state="translated">隱藏的診斷識別碼 '{0}' 不符合指定隱藏描述項的可隱藏識別碼 '{1}'。</target>
......@@ -96,8 +106,8 @@
</trans-unit>
<trans-unit id="Single_type_per_generator_0">
<source>Only a single {0} can be registered per generator.</source>
<target state="translated">每個產生器只能註冊一個 {0}。</target>
<note />
<target state="needs-review-translation">每個產生器只能註冊一個 {0}。</target>
<note>{0}: type name</note>
</trans-unit>
<trans-unit id="SupportedDiagnosticsHasNullDescriptor">
<source>Analyzer '{0}' contains a null descriptor in its 'SupportedDiagnostics'.</source>
......
......@@ -62,7 +62,7 @@ private static Project CreateEmptyProject()
Assert.NotSame(originalCompilation, newCompilation);
var generatedTree = Assert.Single(newCompilation.SyntaxTrees);
Assert.Equal("Test.generated", Path.GetFileName(generatedTree.FilePath));
Assert.Equal($"{typeof(AdditionalFileAddedGenerator).Module.ModuleVersionId}_{typeof(AdditionalFileAddedGenerator).FullName}_Test.generated.cs", Path.GetFileName(generatedTree.FilePath));
}
[Theory]
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册