diff --git a/src/Controls/src/SourceGen/CodeBehindGenerator.cs b/src/Controls/src/SourceGen/CodeBehindGenerator.cs index 478aef991f59..e6b5aab713dc 100644 --- a/src/Controls/src/SourceGen/CodeBehindGenerator.cs +++ b/src/Controls/src/SourceGen/CodeBehindGenerator.cs @@ -96,12 +96,11 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext) initContext.RegisterSourceOutput(xamlSourceProviderForCB, static (sourceProductionContext, provider) => { var (xamlItem, xmlnsCache, typeCache, compilation) = provider; - var fileName = $"{(string.IsNullOrEmpty(Path.GetDirectoryName(xamlItem!.ProjectItem!.TargetPath)) ? "" : Path.GetDirectoryName(xamlItem.ProjectItem.TargetPath) + Path.DirectorySeparatorChar)}{Path.GetFileNameWithoutExtension(xamlItem.ProjectItem.TargetPath)}.{xamlItem.ProjectItem.Kind.ToLowerInvariant()}.sg.cs".Replace(Path.DirectorySeparatorChar, '_'); try { var code = CodeBehindCodeWriter.GenerateXamlCodeBehind(xamlItem, compilation, sourceProductionContext.ReportDiagnostic, sourceProductionContext.CancellationToken, xmlnsCache, typeCache); - sourceProductionContext.AddSource(fileName, code); + sourceProductionContext.AddSource(GetHintName(xamlItem?.ProjectItem, "sg"), code); } catch (Exception e) { @@ -115,7 +114,10 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext) { var (xamlItem, xmlnsCache, typeCache, compilation) = provider; - var fileName = $"{(string.IsNullOrEmpty(Path.GetDirectoryName(xamlItem!.ProjectItem!.TargetPath)) ? "" : Path.GetDirectoryName(xamlItem.ProjectItem.TargetPath) + Path.DirectorySeparatorChar)}{Path.GetFileNameWithoutExtension(xamlItem.ProjectItem.TargetPath)}.{xamlItem.ProjectItem.Kind.ToLowerInvariant()}.xsg.cs".Replace(Path.DirectorySeparatorChar, '_'); + if (xamlItem?.ProjectItem?.RelativePath is not string relativePath) + { + throw new InvalidOperationException("Xaml item or target path is null"); + } if (!ShouldGenerateSourceGenInitializeComponent(xamlItem, xmlnsCache, compilation)) return; @@ -125,7 +127,7 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext) if (xamlItem != null && xamlItem.Exception != null) { var lineInfo = xamlItem.Exception is XamlParseException xpe ? xpe.XmlInfo : new XmlLineInfo(); - var location = LocationCreate(fileName, lineInfo, string.Empty); + var location = LocationCreate(relativePath, lineInfo, string.Empty); sourceProductionContext.ReportDiagnostic(Diagnostic.Create(Descriptors.XamlParserError, location, xamlItem.Exception.Message)); } return; @@ -137,14 +139,13 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext) return; var code = InitializeComponentCodeWriter.GenerateInitializeComponent(xamlItem, compilation, sourceProductionContext, xmlnsCache, typeCache); - sourceProductionContext.AddSource(fileName, code); + sourceProductionContext.AddSource(GetHintName(xamlItem.ProjectItem, "xsg"), code); } catch (Exception e) { var location = xamlItem?.ProjectItem?.RelativePath is not null ? Location.Create(xamlItem.ProjectItem.RelativePath, new TextSpan(), new LinePositionSpan()) : null; sourceProductionContext.ReportDiagnostic(Diagnostic.Create(Descriptors.XamlParserError, location, e.Message)); } - }); // Register the CSS pipeline @@ -184,6 +185,19 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext) }); } + private static string GetHintName(ProjectItem? projectItem, string suffix) + { + if (projectItem?.RelativePath is not string relativePath) + { + throw new InvalidOperationException("Project item or target path is null"); + } + + var prefix = Path.GetDirectoryName(relativePath).Replace(Path.DirectorySeparatorChar, '_').Replace(':', '_'); + var fileNameNoExtension = Path.GetFileNameWithoutExtension(relativePath); + var kind = projectItem.Kind.ToLowerInvariant() ?? "unknown-kind"; + return $"{prefix}{fileNameNoExtension}.{kind}.{suffix}.cs"; + } + private static string? GenerateGlobalXmlns(SourceProductionContext sourceProductionContext, AssemblyCaches xmlnsCache) { if (xmlnsCache.GlobalGeneratedXmlnsDefinitions.Count == 0) diff --git a/src/Controls/src/SourceGen/PrePost.cs b/src/Controls/src/SourceGen/PrePost.cs index fed5b62057b0..d28aef7b36d2 100644 --- a/src/Controls/src/SourceGen/PrePost.cs +++ b/src/Controls/src/SourceGen/PrePost.cs @@ -15,15 +15,19 @@ class PrePost : IDisposable /// /// /// - public static PrePost NewLineInfo(IndentedTextWriter codeWriter, IXmlLineInfo iXmlLineInfo, string? fileName) + public static PrePost NewLineInfo(IndentedTextWriter codeWriter, IXmlLineInfo iXmlLineInfo, ProjectItem? projectItem) { - static void LineInfo(IndentedTextWriter codeWriter, IXmlLineInfo iXmlLineInfo, string? fileName) - => codeWriter.WriteLineNoTabs($"#line {(iXmlLineInfo.LineNumber != -1 ? iXmlLineInfo.LineNumber : 1)} \"{fileName}\""); + // Emit #line with an absolute path since relative paths have undefined behavior (https://github.com/dotnet/roslyn/issues/71202#issuecomment-1874649780) + static void LineInfo(IndentedTextWriter codeWriter, IXmlLineInfo iXmlLineInfo, ProjectItem? projectItem) + { + var lineNumber = iXmlLineInfo.LineNumber != -1 ? iXmlLineInfo.LineNumber : 1; + codeWriter.WriteLineNoTabs($"#line {lineNumber} \"{projectItem?.TargetPath}\""); + } static void LineDefault(IndentedTextWriter codeWriter, IXmlLineInfo iXmlLineInfo) => codeWriter.WriteLineNoTabs("#line default"); - return new(() => LineInfo(codeWriter, iXmlLineInfo, fileName), () => LineDefault(codeWriter, iXmlLineInfo)); + return new(() => LineInfo(codeWriter, iXmlLineInfo, projectItem), () => LineDefault(codeWriter, iXmlLineInfo)); } public static PrePost NoBlock() => diff --git a/src/Controls/src/SourceGen/ProjectItem.cs b/src/Controls/src/SourceGen/ProjectItem.cs index 7a935f87ec40..63c18b382a55 100644 --- a/src/Controls/src/SourceGen/ProjectItem.cs +++ b/src/Controls/src/SourceGen/ProjectItem.cs @@ -7,6 +7,8 @@ namespace Microsoft.Maui.Controls.SourceGen; record ProjectItem(AdditionalText AdditionalText, AnalyzerConfigOptions Options) { + private readonly AdditionalText _additionalText = AdditionalText; + public string Configuration => Options.GetValueOrDefault("build_property.Configuration", "Debug"); @@ -66,5 +68,5 @@ public string? TargetFramework => Options.GetValueOrNull("build_property.targetFramework"); public string? TargetPath - => Options.GetValueOrDefault("build_metadata.additionalfiles.TargetPath", AdditionalText.Path); + => Options.GetValueOrDefault("build_metadata.additionalfiles.TargetPath", _additionalText.Path); } diff --git a/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs b/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs index b0ac5db8e972..0522c3a43415 100644 --- a/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs +++ b/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs @@ -474,14 +474,14 @@ static void Set(IndentedTextWriter writer, LocalVariable parentVar, string local if (node is ValueNode valueNode) { - using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)node, context.ProjectItem.RelativePath) : PrePost.NoBlock()) + using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)node, context.ProjectItem) : PrePost.NoBlock()) { var valueString = valueNode.ConvertTo(property, context, parentVar); writer.WriteLine($"{parentVar.Name}.{EscapeIdentifier(localName)} = {valueString};"); } } else if (node is ElementNode elementNode) - using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)node, context.ProjectItem.RelativePath) : PrePost.NoBlock()) + using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)node, context.ProjectItem) : PrePost.NoBlock()) writer.WriteLine($"{parentVar.Name}.{EscapeIdentifier(localName)} = ({property.Type.ToFQDisplayString()}){(HasDoubleImplicitConversion(context.Variables[elementNode].Type, property.Type, context, out var conv) ? "(" + conv!.ReturnType.ToFQDisplayString() + ")" : string.Empty)}{context.Variables[elementNode].Name};"); } @@ -610,7 +610,7 @@ static void Add(IndentedTextWriter writer, LocalVariable parentVar, XmlName prop if (HasDoubleImplicitConversion(context.Variables[valueNode].Type, itemType, context, out var conv)) cast = "(" + conv!.ReturnType.ToFQDisplayString() + ")"; - using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)valueNode, context.ProjectItem.RelativePath) : PrePost.NoBlock()) + using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)valueNode, context.ProjectItem) : PrePost.NoBlock()) writer.WriteLine($"{parentObj}.Add(({itemType.ToFQDisplayString()}){cast}{context.Variables[valueNode].Name});"); } diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs index 84cd5c7f4ff7..40db7a803a23 100644 --- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs @@ -1,3 +1,5 @@ +using System; +using System.IO; using System.Linq; using NUnit.Framework; @@ -50,9 +52,8 @@ public struct Bar public string Title { get; set; } = "Title"; } """; - - var expected = -""" + var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml"); + var expected = $$""" //------------------------------------------------------------------------------ // @@ -81,7 +82,7 @@ private partial void InitializeComponent() #if !_MAUIXAML_SG_NAMESCOPE_DISABLE global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope); #endif -#line 1 "Test.xaml" +#line 1 "{{testXamlFilePath}}" bindingExtension.Path = "Foo.Bar.Title"; #line default var bindingBase = CreateTypedBindingFrom_bindingExtension(bindingExtension); diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/LineInfoTests.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/LineInfoTests.cs new file mode 100644 index 000000000000..58602d6b395e --- /dev/null +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/LineInfoTests.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using System.Linq; +using NUnit.Framework; + +namespace Microsoft.Maui.Controls.SourceGen.UnitTests.InitializeComponent; + +public class LineInfoTests : SourceGenXamlInitializeComponentTestBase +{ + [Test] + public void DiagnosticShowsLocationInInputXamlFile() + { + var xaml = +""" + + + + + + + + + + +"""; + + var (result, _) = RunGenerator(xaml, string.Empty); + + var generatedCode = result.GeneratedTrees.Single(tree => Path.GetFileName(tree.FilePath) == "Test.xaml.xsg.cs").ToString(); + var expectedFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml"); + Assert.IsTrue(generatedCode.Contains(@$"#line 9 ""{expectedFilePath}""", StringComparison.Ordinal)); + } +} diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SetBinding.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SetBinding.cs index 994df67db456..1e3e29253e94 100644 --- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SetBinding.cs +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SetBinding.cs @@ -1,3 +1,5 @@ +using System; +using System.IO; using System.Linq; using NUnit.Framework; @@ -36,8 +38,8 @@ public TestPage() } """; - var expected = -""" + var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml"); + var expected = $$""" //------------------------------------------------------------------------------ // @@ -66,7 +68,7 @@ private partial void InitializeComponent() #if !_MAUIXAML_SG_NAMESCOPE_DISABLE global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope); #endif -#line 1 "Test.xaml" +#line 1 "{{testXamlFilePath}}" bindingExtension.Path = "Title"; #line default var bindingBase = new global::Microsoft.Maui.Controls.Binding(bindingExtension.Path, bindingExtension.Mode, bindingExtension.Converter, bindingExtension.ConverterParameter, bindingExtension.StringFormat, bindingExtension.Source) { UpdateSourceEventName = bindingExtension.UpdateSourceEventName, FallbackValue = bindingExtension.FallbackValue, TargetNullValue = bindingExtension.TargetNullValue }; diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs index f23faac3404e..ffbe73ea692f 100644 --- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using NUnit.Framework; @@ -43,8 +44,8 @@ public TestPage() } """; -var expected = -""" + var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml"); + var expected = $$""" //------------------------------------------------------------------------------ // @@ -97,7 +98,7 @@ private partial void InitializeComponent() #if !_MAUIXAML_SG_NAMESCOPE_DISABLE global::Microsoft.Maui.Controls.Internals.INameScope iNameScope2 = new global::Microsoft.Maui.Controls.Internals.NameScope(); #endif -#line 8 "Test.xaml" +#line 8 "{{testXamlFilePath}}" var xamlServiceProvider1 = new global::Microsoft.Maui.Controls.Xaml.Internals.XamlServiceProvider(this); var iProvideValueTarget1 = new global::Microsoft.Maui.Controls.Xaml.Internals.SimpleValueTargetProvider( new object?[] {setter, style1, __root}, @@ -118,16 +119,16 @@ private partial void InitializeComponent() xamlServiceProvider1.Add(typeof(global::Microsoft.Maui.Controls.Xaml.IXamlTypeResolver), new global::Microsoft.Maui.Controls.Xaml.Internals.XamlTypeResolver(xmlNamespaceResolver1, typeof(global::Test.TestPage).Assembly)); setter.Property = ((global::Microsoft.Maui.Controls.IExtendedTypeConverter)new global::Microsoft.Maui.Controls.BindablePropertyConverter()).ConvertFromInvariantString("TextColor", xamlServiceProvider1) as global::Microsoft.Maui.Controls.BindableProperty; #line default -#line 8 "Test.xaml" +#line 8 "{{testXamlFilePath}}" setter.Value = "Pink"; #line default var setter2 = new global::Microsoft.Maui.Controls.Setter {Property = global::Microsoft.Maui.Controls.Label.TextColorProperty, Value = global::Microsoft.Maui.Graphics.Color.Parse("Pink")}; if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(setter2!) == null) global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(setter2!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 14); -#line 8 "Test.xaml" +#line 8 "{{testXamlFilePath}}" ((global::System.Collections.Generic.ICollection)style1.Setters).Add((global::Microsoft.Maui.Controls.Setter)setter2); #line default -#line 9 "Test.xaml" +#line 9 "{{testXamlFilePath}}" var xamlServiceProvider2 = new global::Microsoft.Maui.Controls.Xaml.Internals.XamlServiceProvider(this); var iProvideValueTarget2 = new global::Microsoft.Maui.Controls.Xaml.Internals.SimpleValueTargetProvider( new object?[] {setter1, style1, __root}, @@ -148,13 +149,13 @@ private partial void InitializeComponent() xamlServiceProvider2.Add(typeof(global::Microsoft.Maui.Controls.Xaml.IXamlTypeResolver), new global::Microsoft.Maui.Controls.Xaml.Internals.XamlTypeResolver(xmlNamespaceResolver2, typeof(global::Test.TestPage).Assembly)); setter1.Property = ((global::Microsoft.Maui.Controls.IExtendedTypeConverter)new global::Microsoft.Maui.Controls.BindablePropertyConverter()).ConvertFromInvariantString("IsVisible", xamlServiceProvider2) as global::Microsoft.Maui.Controls.BindableProperty; #line default -#line 1 "Test.xaml" +#line 1 "{{testXamlFilePath}}" setter1.Value = "True"; #line default var setter3 = new global::Microsoft.Maui.Controls.Setter {Property = global::Microsoft.Maui.Controls.VisualElement.IsVisibleProperty, Value = (bool)new global::Microsoft.Maui.Controls.VisualElement.VisibilityConverter().ConvertFromInvariantString("True")!}; if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(setter3!) == null) global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(setter3!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 9, 14); -#line 9 "Test.xaml" +#line 9 "{{testXamlFilePath}}" ((global::System.Collections.Generic.ICollection)style1.Setters).Add((global::Microsoft.Maui.Controls.Setter)setter3); #line default var resourceDictionary = __root.Resources; diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SourceGenXamlInitializeComponentTests.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SourceGenXamlInitializeComponentTests.cs index c546788146d5..887984940dde 100644 --- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SourceGenXamlInitializeComponentTests.cs +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SourceGenXamlInitializeComponentTests.cs @@ -1,3 +1,5 @@ +using System; +using System.IO; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -16,7 +18,9 @@ protected record AdditionalXamlFile(string Path, string Content, string? Relativ { var compilation = CreateMauiCompilation(); compilation = compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(code)); - var result = RunGenerator(compilation, new AdditionalXamlFile("Test.xaml", xaml, TargetFramework: targetFramework, NoWarn: noWarn)); + var workingDirectory = Environment.CurrentDirectory; + var xamlFile = new AdditionalXamlFile(Path.Combine(workingDirectory, "Test.xaml"), xaml, RelativePath: "Test.xaml", TargetFramework: targetFramework, NoWarn: noWarn); + var result = RunGenerator(compilation, xamlFile); var generated = result.Results.SingleOrDefault().GeneratedSources.SingleOrDefault(gs => gs.HintName.EndsWith(".xsg.cs")).SourceText?.ToString(); return (result, generated); diff --git a/src/Controls/tests/SourceGen.UnitTests/SourceGeneratorDriver.cs b/src/Controls/tests/SourceGen.UnitTests/SourceGeneratorDriver.cs index 85259d23cb16..60fc6fdec84f 100644 --- a/src/Controls/tests/SourceGen.UnitTests/SourceGeneratorDriver.cs +++ b/src/Controls/tests/SourceGen.UnitTests/SourceGeneratorDriver.cs @@ -94,6 +94,7 @@ private static MetadataReference[] GetMauiReferences() MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "System.Private.CoreLib.dll")), MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "System.Runtime.dll")), MetadataReference.CreateFromFile(Path.Combine(dotNetAssemblyPath, "System.ObjectModel.dll")), + MetadataReference.CreateFromFile(typeof(Uri).Assembly.Location), //System.Private.Uri MetadataReference.CreateFromFile(typeof(Color).Assembly.Location), //Graphics MetadataReference.CreateFromFile(typeof(Button).Assembly.Location), //Controls MetadataReference.CreateFromFile(typeof(BindingExtension).Assembly.Location), //Xaml