File: XamlCTask.cs
Web Access
Project: src\src\Controls\src\Build.Tasks\Controls.Build.Tasks.csproj (Microsoft.Maui.Controls.Build.Tasks)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Xml;
using Microsoft.Build.Utilities;
using Microsoft.Maui.Controls.Xaml;
using Mono.Cecil;
using Mono.Cecil.Cil;
using static Microsoft.Build.Framework.MessageImportance;
using static Mono.Cecil.Cil.OpCodes;
using IOPath = System.IO.Path;
 
namespace Microsoft.Maui.Controls.Build.Tasks
{
	/// <summary>
	/// Provides extension methods for the <see cref="TaskLoggingHelper"/> class to assist with logging warnings and errors.
	/// </summary>
	static class LoggingHelperExtensions
	{
		class LoggingHelperContext
		{
			public int WarningLevel { get; set; } = 4; //unused so far
			public bool TreatWarningsAsErrors { get; set; } = false;
			public IList<int> WarningsAsErrors { get; set; }
			public IList<int> WarningsNotAsErrors { get; set; }
			public IList<int> NoWarn { get; set; }
			public string PathPrefix { get; set; }
		}
 
		static LoggingHelperContext Context { get; set; }
		internal static List<BuildException> LoggedErrors { get; set; }
 
		public static void SetContext(
			this TaskLoggingHelper loggingHelper,
			int warningLevel,
			bool treatWarningsAsErrors,
			string noWarn,
			string warningsAsErrors,
			string warningsNotAsErrors,
			string pathPrefix)
		{
			if (Context == null)
				Context = new LoggingHelperContext();
			Context.WarningLevel = warningLevel;
			Context.TreatWarningsAsErrors = treatWarningsAsErrors;
			Context.PathPrefix = pathPrefix;
 
			Context.NoWarn = noWarn?.Split([';', ','], StringSplitOptions.RemoveEmptyEntries).Select(s =>
			{
				if (int.TryParse(s, out var i))
					return i;
				if (s.StartsWith("XC"))
				{
					var code = s.Substring(2);
					if (int.TryParse(code, out i))
						return i;
				}
				return -1;
			}).Where(i => i != -1).ToList();
 
			Context.WarningsAsErrors = warningsAsErrors?.Split([';', ','], StringSplitOptions.RemoveEmptyEntries).Select(s =>
			{
				if (int.TryParse(s, out var i))
					return i;
				if (s.StartsWith("XC"))
				{
					var code = s.Substring(2);
					if (int.TryParse(code, out i))
						return i;
				}
				return -1;
			}).Where(i => i != -1).ToList();
 
			Context.WarningsNotAsErrors = warningsNotAsErrors?.Split([';', ','], StringSplitOptions.RemoveEmptyEntries).Select(s =>
			{
				if (int.TryParse(s, out var i))
					return i;
				if (s.StartsWith("XC"))
				{
					var code = s.Substring(2);
					if (int.TryParse(code, out i))
						return i;
				}
				return -1;
			}).Where(i => i != -1).ToList();
		}
 
		public static void LogWarningOrError(this TaskLoggingHelper loggingHelper, BuildExceptionCode code, string xamlFilePath, int lineNumber, int linePosition, int endLineNumber, int endLinePosition, params object[] messageArgs)
		{
			if (Context == null)
				Context = new LoggingHelperContext();
			if (Context.NoWarn != null && Context.NoWarn.Contains(code.CodeCode))
				return;
			xamlFilePath = loggingHelper.GetXamlFilePath(xamlFilePath);
			if ((Context.TreatWarningsAsErrors && (Context.WarningsNotAsErrors == null || !Context.WarningsNotAsErrors.Contains(code.CodeCode)))
				|| (Context.WarningsAsErrors != null && Context.WarningsAsErrors.Contains(code.CodeCode)))
			{
				loggingHelper.LogError("XamlC", $"{code.CodePrefix}{code.CodeCode:0000}", code.HelpLink, xamlFilePath, lineNumber, linePosition, endLineNumber, endLinePosition, ErrorMessages.ResourceManager.GetString(code.ErrorMessageKey), messageArgs);
				LoggedErrors ??= new();
				LoggedErrors.Add(new BuildException(code, new XmlLineInfo(lineNumber, linePosition), innerException: null, messageArgs));
			}
			else
			{
				loggingHelper.LogWarning("XamlC", $"{code.CodePrefix}{code.CodeCode:0000}", code.HelpLink, xamlFilePath, lineNumber, linePosition, endLineNumber, endLinePosition, ErrorMessages.ResourceManager.GetString(code.ErrorMessageKey), messageArgs);
			}
		}
 
		public static string GetXamlFilePath(this TaskLoggingHelper loggingHelper, string xamlFilePath)
		{
			Context ??= new LoggingHelperContext();
 
			if (Context.PathPrefix is string prefix)
			{
				xamlFilePath = IOPath.Combine(prefix, xamlFilePath);
			}
 
			return xamlFilePath;
		}
	}
 
	public class XamlCTask : XamlTask
	{
		readonly XamlCache cache = new();
		bool hasCompiledXamlResources;
		public bool KeepXamlResources { get; set; }
		public bool OptimizeIL { get; set; } = true;
		public bool DefaultCompile { get; set; }
		public bool ForceCompile { get; set; }
		public bool CompileBindingsWithSource { get; set; }
		public string TargetFramework { get; set; }
 
		public int WarningLevel { get; set; } = 4; //unused so far
		public bool TreatWarningsAsErrors { get; set; } = false;
		public string WarningsAsErrors { get; set; }
		public string WarningsNotAsErrors { get; set; }
		public string NoWarn { get; set; }
 
		public bool GenerateFullPaths { get; set; }
		public string FullPathPrefix { get; set; }
 
		public IAssemblyResolver DefaultAssemblyResolver { get; set; }
 
		internal string Type { get; set; }
		internal MethodDefinition InitCompForType { get; private set; }
 
		/// <summary>
		/// Enable to optimize for shorter build time
		/// e.g. OptimizeIL unused, Debug symbols not loaded, no assemblies written
		/// </summary>
		public bool ValidateOnly { get; set; }
 
		internal bool GenerateFullILInValidateOnlyMode { get; set; }
 
		public override bool Execute(out IList<Exception> thrownExceptions)
		{
			thrownExceptions = null;
			LoggingHelper.SetContext(WarningLevel, TreatWarningsAsErrors, NoWarn, WarningsAsErrors, WarningsNotAsErrors, GenerateFullPaths ? FullPathPrefix : null);
			LoggingHelper.LogMessage(Normal, $"{new string(' ', 0)}Compiling Xaml, assembly: {Assembly}");
			var skipassembly = !DefaultCompile;
			bool success = true;
 
			if (!File.Exists(Assembly))
			{
				LoggingHelper.LogMessage(Normal, $"{new string(' ', 2)}Assembly file not found. Skipping XamlC.");
				return true;
			}
 
			if (GenerateFullPaths && string.IsNullOrEmpty(FullPathPrefix))
			{
				LoggingHelper.LogMessage(Low, "  GenerateFullPaths is enabled but FullPathPrefix is missing or empty.");
			}
 
			using (var fallbackResolver = DefaultAssemblyResolver == null ? new XamlCAssemblyResolver() : null)
			{
				var resolver = DefaultAssemblyResolver ?? fallbackResolver;
				if (resolver is XamlCAssemblyResolver xamlCResolver)
				{
					if (ReferencePath != null)
					{
						var paths = ReferencePath.Select(p => IOPath.GetDirectoryName(p.Replace("//", "/"))).Distinct();
						foreach (var searchpath in paths)
						{
							LoggingHelper.LogMessage(Low, $"{new string(' ', 2)}Adding searchpath {searchpath}");
							xamlCResolver.AddSearchDirectory(searchpath);
						}
					}
				}
				else
					LoggingHelper.LogMessage(Low, $"{new string(' ', 2)}Ignoring dependency and reference paths due to an unsupported resolver");
 
				var debug = DebugSymbols || (!string.IsNullOrEmpty(DebugType) && DebugType.ToLowerInvariant() != "none");
 
				var readerParameters = new ReaderParameters
				{
					AssemblyResolver = resolver,
					ReadWrite = !ValidateOnly,
					ReadSymbols = debug && !ValidateOnly, // We don't need symbols for ValidateOnly, since we won't be writing
				};
 
				using (var assemblyDefinition = AssemblyDefinition.ReadAssembly(IOPath.GetFullPath(Assembly), readerParameters))
				{
					CustomAttribute xamlcAttr;
					if (assemblyDefinition.HasCustomAttributes &&
						(xamlcAttr =
							assemblyDefinition.CustomAttributes.FirstOrDefault(
								ca => ca.AttributeType.FullName == "Microsoft.Maui.Controls.Xaml.XamlCompilationAttribute")) != null)
					{
						var options = (XamlCompilationOptions)xamlcAttr.ConstructorArguments[0].Value;
						if ((options & XamlCompilationOptions.Skip) == XamlCompilationOptions.Skip)
							skipassembly = true;
						if ((options & XamlCompilationOptions.Compile) == XamlCompilationOptions.Compile)
							skipassembly = false;
					}
 
					foreach (var module in assemblyDefinition.Modules)
					{
						var skipmodule = skipassembly;
						if (module.HasCustomAttributes &&
							(xamlcAttr =
								module.CustomAttributes.FirstOrDefault(
									ca => ca.AttributeType.FullName == "Microsoft.Maui.Controls.Xaml.XamlCompilationAttribute")) != null)
						{
							var options = (XamlCompilationOptions)xamlcAttr.ConstructorArguments[0].Value;
							if ((options & XamlCompilationOptions.Skip) == XamlCompilationOptions.Skip)
								skipmodule = true;
							if ((options & XamlCompilationOptions.Compile) == XamlCompilationOptions.Compile)
								skipmodule = false;
						}
 
						LoggingHelper.LogMessage(Low, $"{new string(' ', 2)}Module: {module.Name}");
						var resourcesToPrune = new List<EmbeddedResource>();
						foreach (var resource in module.Resources.OfType<EmbeddedResource>())
						{
							LoggingHelper.LogMessage(Low, $"{new string(' ', 4)}Resource: {resource.Name}");
							string classname;
							if (!resource.IsXaml(cache, module, out classname))
							{
								LoggingHelper.LogMessage(Low, $"{new string(' ', 6)}skipped.");
								continue;
							}
							TypeDefinition typeDef = module.GetType(classname);
							if (typeDef == null)
							{
								LoggingHelper.LogMessage(Low, $"{new string(' ', 6)}no type found... skipped.");
								continue;
							}
							var skiptype = skipmodule;
							if (typeDef.HasCustomAttributes &&
								(xamlcAttr =
									typeDef.CustomAttributes.FirstOrDefault(
										ca => ca.AttributeType.FullName == "Microsoft.Maui.Controls.Xaml.XamlCompilationAttribute")) != null)
							{
								var options = (XamlCompilationOptions)xamlcAttr.ConstructorArguments[0].Value;
								if ((options & XamlCompilationOptions.Skip) == XamlCompilationOptions.Skip)
									skiptype = true;
								if ((options & XamlCompilationOptions.Compile) == XamlCompilationOptions.Compile)
									skiptype = false;
							}
 
							if (Type != null)
								skiptype = !(Type == classname);
 
							if (skiptype && !ForceCompile)
							{
								LoggingHelper.LogMessage(Low, $"{new string(' ', 6)}has XamlCompilationAttribute set to Skip and not Compile... skipped.");
								continue;
							}
 
							var initComp = typeDef.Methods.FirstOrDefault(md => md.Name == "InitializeComponent");
							if (initComp == null)
							{
								LoggingHelper.LogMessage(Low, $"{new string(' ', 6)}no InitializeComponent found... skipped.");
								continue;
							}
 
							CustomAttribute xamlFilePathAttr;
							var xamlFilePath = typeDef.HasCustomAttributes && (xamlFilePathAttr = typeDef.CustomAttributes.FirstOrDefault(ca => ca.AttributeType.FullName == "Microsoft.Maui.Controls.Xaml.XamlFilePathAttribute")) != null ?
													  (string)xamlFilePathAttr.ConstructorArguments[0].Value :
													  resource.Name;
 
 
							LoggingHelper.LogMessage(Low, $"{new string(' ', 6)}Parsing Xaml");
							ILRootNode rootnode = null;
							try {
								rootnode = ParseXaml(resource.GetResourceStream(), typeDef);
								if (rootnode == null)
								{
									LoggingHelper.LogMessage(Low, $"{new string(' ', 8)}failed.");
									continue;
								}
							} catch (XamlParseException xpe)
							{
								LoggingHelper.LogMessage(Low, $"{new string(' ', 8)}failed.");
								xamlFilePath = LoggingHelper.GetXamlFilePath(xamlFilePath);
								LoggingHelper.LogError("XamlC", null, xpe.HelpLink, xamlFilePath, xpe.XmlInfo.LineNumber, xpe.XmlInfo.LinePosition, 0, 0, xpe.UnformattedMessage);
								continue;
							}
							LoggingHelper.LogMessage(Low, $"{new string(' ', 8)}done.");
 
							hasCompiledXamlResources = true;
 
							LoggingHelper.LogMessage(Low, $"{new string(' ', 6)}Replacing {0}.InitializeComponent ()");
							Exception e;
							if (!TryCoreCompile(initComp, rootnode, xamlFilePath, LoggingHelper, out e))
							{
								success = false;
								LoggingHelper.LogMessage(Low, $"{new string(' ', 8)}failed.");
								(thrownExceptions = thrownExceptions ?? new List<Exception>()).Add(e);
								xamlFilePath = LoggingHelper.GetXamlFilePath(xamlFilePath);
								if (e is BuildException be)
									LoggingHelper.LogError("XamlC", be.Code.Code, be.HelpLink, xamlFilePath, be.XmlInfo?.LineNumber ?? 0, be.XmlInfo?.LinePosition ?? 0, 0, 0, ErrorMessages.ResourceManager.GetString(be.Code.ErrorMessageKey), be.MessageArgs);
								else if (e is XamlParseException xpe) //shouldn't happen anymore
									LoggingHelper.LogError("XamlC", null, xpe.HelpLink, xamlFilePath, xpe.XmlInfo.LineNumber, xpe.XmlInfo.LinePosition, 0, 0, xpe.Message);
								else if (e is XmlException xe)
									LoggingHelper.LogError("XamlC", null, xe.HelpLink, xamlFilePath, xe.LineNumber, xe.LinePosition, 0, 0, xe.Message);
								else
									LoggingHelper.LogError("XamlC", null, e.HelpLink, xamlFilePath, 0, 0, 0, 0, e.Message);
								LoggingHelper.LogMessage(Low, e.StackTrace);
								continue;
							}
							else
							{
								if (LoggingHelperExtensions.LoggedErrors is List<BuildException> errors)
								{
									foreach (var error in errors)
									{
										success = false;
										(thrownExceptions = thrownExceptions ?? new List<Exception>()).Add(error);
									}
 
									LoggingHelperExtensions.LoggedErrors = null;
								}
							}
 
							if (initComp.HasCustomAttributes)
							{
								var suppressMessageAttribute = initComp.CustomAttributes.FirstOrDefault(ca => ca.AttributeType.FullName == "System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute");
								if (suppressMessageAttribute != null)
								{
									LoggingHelper.LogMessage(Low, $"{new string(' ', 6)}Removing UnconditionalSuppressMessageAttribute from InitializeComponent()");
									initComp.CustomAttributes.Remove(suppressMessageAttribute);
								}
							}
							if (Type != null)
								InitCompForType = initComp;
 
							LoggingHelper.LogMessage(Low, $"{new string(' ', 8)}done.");
 
							if (ValidateOnly)
								continue;
 
							if (OptimizeIL)
							{
								LoggingHelper.LogMessage(Low, $"{new string(' ', 6)}Optimizing IL");
								initComp.Body.Optimize();
								LoggingHelper.LogMessage(Low, $"{new string(' ', 8)}done.");
							}
							resourcesToPrune.Add(resource);
						}
						if (hasCompiledXamlResources)
						{
							LoggingHelper.LogMessage(Low, $"{new string(' ', 4)}Changing the module MVID");
							module.Mvid = Guid.NewGuid();
							LoggingHelper.LogMessage(Low, $"{new string(' ', 6)}done.");
						}
						if (!KeepXamlResources)
						{
							if (resourcesToPrune.Any())
								LoggingHelper.LogMessage(Low, $"{new string(' ', 4)}Removing compiled xaml resources");
							foreach (var resource in resourcesToPrune)
							{
								LoggingHelper.LogMessage(Low, $"{new string(' ', 6)}Removing {resource.Name}");
								module.Resources.Remove(resource);
								LoggingHelper.LogMessage(Low, $"{new string(' ', 8)}done.");
							}
						}
					}
					if (ValidateOnly)
					{
						LoggingHelper.LogMessage(Low, $"{new string(' ', 0)}ValidateOnly=True. Skipping writing assembly.");
						return success;
					}
					if (!hasCompiledXamlResources)
					{
						LoggingHelper.LogMessage(Low, $"{new string(' ', 0)}No compiled resources. Skipping writing assembly.");
						return success;
					}
 
					LoggingHelper.LogMessage(Low, $"{new string(' ', 0)}Writing the assembly");
					try
					{
						assemblyDefinition.Write(new WriterParameters
						{
							WriteSymbols = debug,
						});
						LoggingHelper.LogMessage(Low, $"{new string(' ', 2)}done.");
					}
					catch (Exception e)
					{
						LoggingHelper.LogMessage(Low, $"{new string(' ', 2)}failed.");
						LoggingHelper.LogErrorFromException(e);
						(thrownExceptions = thrownExceptions ?? new List<Exception>()).Add(e);
						LoggingHelper.LogMessage(Low, e.StackTrace);
						success = false;
					}
				}
			}
			return success;
		}
 
		bool TryCoreCompile(MethodDefinition initComp, ILRootNode rootnode, string xamlFilePath, TaskLoggingHelper loggingHelper, out Exception exception)
		{
			try
			{
				var body = new MethodBody(initComp);
				var module = body.Method.Module;
				body.InitLocals = true;
				var il = body.GetILProcessor();
				var resourcePath = GetPathForType(cache, module, initComp.DeclaringType);
 
				il.Emit(Nop);
 
				var visitorContext = new ILContext(il, body, module, cache)
				{
					XamlFilePath = xamlFilePath,
					LoggingHelper = loggingHelper,
					ValidateOnly = ValidateOnly && !GenerateFullILInValidateOnlyMode,
					CompileBindingsWithSource = CompileBindingsWithSource,
				};
 
 
				rootnode.Accept(new XamlNodeVisitor((node, parent) => node.Parent = parent), null);
				rootnode.Accept(new ExpandMarkupsVisitor(visitorContext), null);
				rootnode.Accept(new PruneIgnoredNodesVisitor(), null);
				rootnode.Accept(new SimplifyOnPlatformVisitor(TargetFramework), null);
				rootnode.Accept(new CreateObjectVisitor(visitorContext), null);
				rootnode.Accept(new SetNamescopesAndRegisterNamesVisitor(visitorContext), null);
				rootnode.Accept(new SetFieldVisitor(visitorContext), null);
				rootnode.Accept(new SimplifyTypeExtensionVisitor(), null);
				rootnode.Accept(new SetResourcesVisitor(visitorContext), null);
				rootnode.Accept(new SetPropertiesVisitor(visitorContext, true), null);
 
				il.Emit(Ret);
				initComp.Body = body;
				exception = null;
				return true;
			}
			catch (Exception e)
			{
				exception = e;
				return false;
			}
		}
 
		internal static string GetPathForType(XamlCache cache, ModuleDefinition module, TypeReference type)
		{
			foreach (var ca in type.Module.GetCustomAttributes())
			{
				if (!TypeRefComparer.Default.Equals(ca.AttributeType, module.ImportReference(cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "XamlResourceIdAttribute"))))
					continue;
				if (!TypeRefComparer.Default.Equals(ca.ConstructorArguments[2].Value as TypeReference, type))
					continue;
				return ca.ConstructorArguments[1].Value as string;
			}
			return null;
		}
 
		internal static string GetResourceIdForPath(XamlCache cache, ModuleDefinition module, string path)
		{
			foreach (var ca in module.GetCustomAttributes())
			{
				if (!TypeRefComparer.Default.Equals(ca.AttributeType, module.ImportReference(cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "XamlResourceIdAttribute"))))
					continue;
				if (ca.ConstructorArguments[1].Value as string != path)
					continue;
				return ca.ConstructorArguments[0].Value as string;
			}
			return null;
		}
	}
}