From 244b46cf7f3252d6dc3884ce184be901d1d173e5 Mon Sep 17 00:00:00 2001 From: Sean Hall Date: Sun, 2 Sep 2018 16:12:29 -0500 Subject: Migrate WixCop into Tools from wix4. --- src/wixcop/CommandLine/ConvertCommand.cs | 212 ++++++++ src/wixcop/CommandLine/HelpCommand.cs | 25 + src/wixcop/CommandLine/WixCopCommandLineParser.cs | 132 +++++ src/wixcop/Converter.cs | 633 ++++++++++++++++++++++ src/wixcop/Interfaces/IWixCopCommandLineParser.cs | 11 + src/wixcop/Program.cs | 67 +++ src/wixcop/WixCop.csproj | 32 ++ 7 files changed, 1112 insertions(+) create mode 100644 src/wixcop/CommandLine/ConvertCommand.cs create mode 100644 src/wixcop/CommandLine/HelpCommand.cs create mode 100644 src/wixcop/CommandLine/WixCopCommandLineParser.cs create mode 100644 src/wixcop/Converter.cs create mode 100644 src/wixcop/Interfaces/IWixCopCommandLineParser.cs create mode 100644 src/wixcop/Program.cs create mode 100644 src/wixcop/WixCop.csproj (limited to 'src/wixcop') diff --git a/src/wixcop/CommandLine/ConvertCommand.cs b/src/wixcop/CommandLine/ConvertCommand.cs new file mode 100644 index 00000000..6af7d4ca --- /dev/null +++ b/src/wixcop/CommandLine/ConvertCommand.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using WixToolset.Extensibility.Data; +using WixToolset.Extensibility.Services; + +namespace WixCop.CommandLine +{ + internal class ConvertCommand : ICommandLineCommand + { + private const string SettingsFileDefault = "wixcop.settings.xml"; + + public ConvertCommand(IServiceProvider serviceProvider, bool fixErrors, int indentationAmount, List searchPatterns, bool subDirectories, string settingsFile1, string settingsFile2) + { + this.ErrorsAsWarnings = new HashSet(); + this.ExemptFiles = new HashSet(); + this.FixErrors = fixErrors; + this.IndentationAmount = indentationAmount; + this.IgnoreErrors = new HashSet(); + this.SearchPatternResults = new HashSet(); + this.SearchPatterns = searchPatterns; + this.ServiceProvider = serviceProvider; + this.SettingsFile1 = settingsFile1; + this.SettingsFile2 = settingsFile2; + this.SubDirectories = subDirectories; + } + + private HashSet ErrorsAsWarnings { get; } + + private HashSet ExemptFiles { get; } + + private bool FixErrors { get; } + + private int IndentationAmount { get; } + + private HashSet IgnoreErrors { get; } + + private HashSet SearchPatternResults { get; } + + private List SearchPatterns { get; } + + private IServiceProvider ServiceProvider { get; } + + private string SettingsFile1 { get; } + + private string SettingsFile2 { get; } + + private bool SubDirectories { get; } + + public int Execute() + { + // parse the settings if any were specified + if (null != this.SettingsFile1 || null != this.SettingsFile2) + { + this.ParseSettingsFiles(this.SettingsFile1, this.SettingsFile2); + } + else + { + if (File.Exists(ConvertCommand.SettingsFileDefault)) + { + this.ParseSettingsFiles(ConvertCommand.SettingsFileDefault, null); + } + } + + var messaging = this.ServiceProvider.GetService(); + var converter = new Converter(messaging, this.IndentationAmount, this.ErrorsAsWarnings, this.IgnoreErrors); + + var errors = this.InspectSubDirectories(converter, Path.GetFullPath(".")); + + foreach (string searchPattern in this.SearchPatterns) + { + if (!this.SearchPatternResults.Contains(searchPattern)) + { + Console.Error.WriteLine("Could not find file \"{0}\"", searchPattern); + errors++; + } + } + + return errors != 0 ? 2 : 0; + } + + /// + /// Get the files that match a search path pattern. + /// + /// The base directory at which to begin the search. + /// The search path pattern. + /// The files matching the pattern. + private static string[] GetFiles(string baseDir, string searchPath) + { + // convert alternate directory separators to the standard one + var filePath = searchPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + var lastSeparator = filePath.LastIndexOf(Path.DirectorySeparatorChar); + string[] files = null; + + try + { + if (0 > lastSeparator) + { + files = Directory.GetFiles(baseDir, filePath); + } + else // found directory separator + { + var searchPattern = filePath.Substring(lastSeparator + 1); + + files = Directory.GetFiles(filePath.Substring(0, lastSeparator + 1), searchPattern); + } + } + catch (DirectoryNotFoundException) + { + // don't let this function throw the DirectoryNotFoundException. (this exception + // occurs for non-existant directories and invalid characters in the searchPattern) + } + + return files; + } + + /// + /// Inspect sub-directories. + /// + /// The directory whose sub-directories will be inspected. + /// The number of errors that were found. + private int InspectSubDirectories(Converter converter, string directory) + { + var errors = 0; + + foreach (var searchPattern in this.SearchPatterns) + { + foreach (var sourceFilePath in GetFiles(directory, searchPattern)) + { + var file = new FileInfo(sourceFilePath); + + if (!this.ExemptFiles.Contains(file.Name.ToUpperInvariant())) + { + this.SearchPatternResults.Add(searchPattern); + errors += converter.ConvertFile(file.FullName, this.FixErrors); + } + } + } + + if (this.SubDirectories) + { + foreach (var childDirectoryPath in Directory.GetDirectories(directory)) + { + errors += this.InspectSubDirectories(converter, childDirectoryPath); + } + } + + return errors; + } + + /// + /// Parse the primary and secondary settings files. + /// + /// The primary settings file. + /// The secondary settings file. + private void ParseSettingsFiles(string localSettingsFile1, string localSettingsFile2) + { + if (null == localSettingsFile1 && null != localSettingsFile2) + { + throw new ArgumentException("Cannot specify a secondary settings file (set2) without a primary settings file (set1).", "localSettingsFile2"); + } + + var settingsFile = localSettingsFile1; + while (null != settingsFile) + { + XmlTextReader reader = null; + try + { + reader = new XmlTextReader(settingsFile); + var doc = new XmlDocument(); + doc.Load(reader); + + // get the types of tests that will have their errors displayed as warnings + var testsIgnoredElements = doc.SelectNodes("/Settings/IgnoreErrors/Test"); + foreach (XmlElement test in testsIgnoredElements) + { + var key = test.GetAttribute("Id"); + this.IgnoreErrors.Add(key); + } + + // get the types of tests that will have their errors displayed as warnings + var testsAsWarningsElements = doc.SelectNodes("/Settings/ErrorsAsWarnings/Test"); + foreach (XmlElement test in testsAsWarningsElements) + { + var key = test.GetAttribute("Id"); + this.ErrorsAsWarnings.Add(key); + } + + // get the exempt files + var localExemptFiles = doc.SelectNodes("/Settings/ExemptFiles/File"); + foreach (XmlElement file in localExemptFiles) + { + var key = file.GetAttribute("Name").ToUpperInvariant(); + this.ExemptFiles.Add(key); + } + } + finally + { + if (null != reader) + { + reader.Close(); + } + } + + settingsFile = localSettingsFile2; + localSettingsFile2 = null; + } + } + } +} diff --git a/src/wixcop/CommandLine/HelpCommand.cs b/src/wixcop/CommandLine/HelpCommand.cs new file mode 100644 index 00000000..a75dac5c --- /dev/null +++ b/src/wixcop/CommandLine/HelpCommand.cs @@ -0,0 +1,25 @@ +using System; +using WixToolset.Extensibility.Data; + +namespace WixCop.CommandLine +{ + internal class HelpCommand : ICommandLineCommand + { + public int Execute() + { + Console.WriteLine(" usage: wixcop.exe sourceFile [sourceFile ...]"); + Console.WriteLine(); + Console.WriteLine(" -f fix errors automatically for writable files"); + Console.WriteLine(" -nologo suppress displaying the logo information"); + Console.WriteLine(" -s search for matching files in current dir and subdirs"); + Console.WriteLine(" -set1 primary settings file"); + Console.WriteLine(" -set2 secondary settings file (overrides primary)"); + Console.WriteLine(" -indent: indentation multiple (overrides default of 4)"); + Console.WriteLine(" -? this help information"); + Console.WriteLine(); + Console.WriteLine(" sourceFile may use wildcards like *.wxs"); + + return 0; + } + } +} diff --git a/src/wixcop/CommandLine/WixCopCommandLineParser.cs b/src/wixcop/CommandLine/WixCopCommandLineParser.cs new file mode 100644 index 00000000..53012cfd --- /dev/null +++ b/src/wixcop/CommandLine/WixCopCommandLineParser.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using WixCop.Interfaces; +using WixToolset.Core; +using WixToolset.Extensibility.Data; +using WixToolset.Extensibility.Services; + +namespace WixCop.CommandLine +{ + public sealed class WixCopCommandLineParser : IWixCopCommandLineParser + { + private bool fixErrors; + private int indentationAmount; + private readonly List searchPatterns; + private readonly IServiceProvider serviceProvider; + private string settingsFile1; + private string settingsFile2; + private bool showHelp; + private bool showLogo; + private bool subDirectories; + + public WixCopCommandLineParser(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + + this.indentationAmount = 4; + this.searchPatterns = new List(); + this.showLogo = true; + } + + public ICommandLineArguments Arguments { get; set; } + + public ICommandLineCommand ParseWixCopCommandLine() + { + this.Parse(); + + if (this.showLogo) + { + AppCommon.DisplayToolHeader(); + Console.WriteLine(); + } + + if (this.showHelp) + { + return new HelpCommand(); + } + + return new ConvertCommand( + this.serviceProvider, + this.fixErrors, + this.indentationAmount, + this.searchPatterns, + this.subDirectories, + this.settingsFile1, + this.settingsFile2); + } + + private void Parse() + { + this.showHelp = 0 == this.Arguments.Arguments.Length; + var parser = this.Arguments.Parse(); + + while (!this.showHelp && + String.IsNullOrEmpty(parser.ErrorArgument) && + parser.TryGetNextSwitchOrArgument(out var arg)) + { + if (String.IsNullOrWhiteSpace(arg)) // skip blank arguments. + { + continue; + } + + if (parser.IsSwitch(arg)) + { + if (!this.ParseArgument(parser, arg)) + { + parser.ErrorArgument = arg; + } + } + else + { + this.searchPatterns.Add(arg); + } + } + } + + private bool ParseArgument(IParseCommandLine parser, string arg) + { + var parameter = arg.Substring(1); + + switch (parameter.ToLowerInvariant()) + { + case "?": + this.showHelp = true; + return true; + case "f": + this.fixErrors = true; + return true; + case "nologo": + this.showLogo = false; + return true; + case "s": + this.subDirectories = true; + return true; + default: // other parameters + if (parameter.StartsWith("set1", StringComparison.Ordinal)) + { + this.settingsFile1 = parameter.Substring(4); + } + else if (parameter.StartsWith("set2", StringComparison.Ordinal)) + { + this.settingsFile2 = parameter.Substring(4); + } + else if (parameter.StartsWith("indent:", StringComparison.Ordinal)) + { + try + { + this.indentationAmount = Convert.ToInt32(parameter.Substring(7)); + } + catch + { + throw new ArgumentException("Invalid numeric argument.", parameter); + } + } + else + { + throw new ArgumentException("Invalid argument.", parameter); + } + return true; + } + } + } +} diff --git a/src/wixcop/Converter.cs b/src/wixcop/Converter.cs new file mode 100644 index 00000000..a204ebe0 --- /dev/null +++ b/src/wixcop/Converter.cs @@ -0,0 +1,633 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixCop +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Xml; + using System.Xml.Linq; + using WixToolset.Data; + using WixToolset.Extensibility.Services; + using WixToolset.Tools.Core; + + /// + /// WiX source code converter. + /// + public class Converter + { + private const string XDocumentNewLine = "\n"; // XDocument normlizes "\r\n" to just "\n". + private static readonly XNamespace WixNamespace = "http://wixtoolset.org/schemas/v4/wxs"; + + private static readonly XName FileElementName = WixNamespace + "File"; + private static readonly XName ExePackageElementName = WixNamespace + "ExePackage"; + private static readonly XName MsiPackageElementName = WixNamespace + "MsiPackage"; + private static readonly XName MspPackageElementName = WixNamespace + "MspPackage"; + private static readonly XName MsuPackageElementName = WixNamespace + "MsuPackage"; + private static readonly XName PayloadElementName = WixNamespace + "Payload"; + private static readonly XName WixElementWithoutNamespaceName = XNamespace.None + "Wix"; + + private static readonly Dictionary OldToNewNamespaceMapping = new Dictionary() + { + { "http://schemas.microsoft.com/wix/BalExtension", "http://wixtoolset.org/schemas/v4/wxs/bal" }, + { "http://schemas.microsoft.com/wix/ComPlusExtension", "http://wixtoolset.org/schemas/v4/wxs/complus" }, + { "http://schemas.microsoft.com/wix/DependencyExtension", "http://wixtoolset.org/schemas/v4/wxs/dependency" }, + { "http://schemas.microsoft.com/wix/DifxAppExtension", "http://wixtoolset.org/schemas/v4/wxs/difxapp" }, + { "http://schemas.microsoft.com/wix/FirewallExtension", "http://wixtoolset.org/schemas/v4/wxs/firewall" }, + { "http://schemas.microsoft.com/wix/GamingExtension", "http://wixtoolset.org/schemas/v4/wxs/gaming" }, + { "http://schemas.microsoft.com/wix/IIsExtension", "http://wixtoolset.org/schemas/v4/wxs/iis" }, + { "http://schemas.microsoft.com/wix/MsmqExtension", "http://wixtoolset.org/schemas/v4/wxs/msmq" }, + { "http://schemas.microsoft.com/wix/NetFxExtension", "http://wixtoolset.org/schemas/v4/wxs/netfx" }, + { "http://schemas.microsoft.com/wix/PSExtension", "http://wixtoolset.org/schemas/v4/wxs/powershell" }, + { "http://schemas.microsoft.com/wix/SqlExtension", "http://wixtoolset.org/schemas/v4/wxs/sql" }, + { "http://schemas.microsoft.com/wix/TagExtension", "http://wixtoolset.org/schemas/v4/wxs/tag" }, + { "http://schemas.microsoft.com/wix/UtilExtension", "http://wixtoolset.org/schemas/v4/wxs/util" }, + { "http://schemas.microsoft.com/wix/VSExtension", "http://wixtoolset.org/schemas/v4/wxs/vs" }, + { "http://wixtoolset.org/schemas/thmutil/2010", "http://wixtoolset.org/schemas/v4/thmutil" }, + { "http://schemas.microsoft.com/wix/2009/Lux", "http://wixtoolset.org/schemas/v4/lux" }, + { "http://schemas.microsoft.com/wix/2006/wi", "http://wixtoolset.org/schemas/v4/wxs" }, + { "http://schemas.microsoft.com/wix/2006/localization", "http://wixtoolset.org/schemas/v4/wxl" }, + { "http://schemas.microsoft.com/wix/2006/libraries", "http://wixtoolset.org/schemas/v4/wixlib" }, + { "http://schemas.microsoft.com/wix/2006/objects", "http://wixtoolset.org/schemas/v4/wixobj" }, + { "http://schemas.microsoft.com/wix/2006/outputs", "http://wixtoolset.org/schemas/v4/wixout" }, + { "http://schemas.microsoft.com/wix/2007/pdbs", "http://wixtoolset.org/schemas/v4/wixpdb" }, + { "http://schemas.microsoft.com/wix/2003/04/actions", "http://wixtoolset.org/schemas/v4/wi/actions" }, + { "http://schemas.microsoft.com/wix/2006/tables", "http://wixtoolset.org/schemas/v4/wi/tables" }, + { "http://schemas.microsoft.com/wix/2006/WixUnit", "http://wixtoolset.org/schemas/v4/wixunit" }, + }; + + private Dictionary> ConvertElementMapping; + + /// + /// Instantiate a new Converter class. + /// + /// Indentation value to use when validating leading whitespace. + /// Test errors to display as warnings. + /// Test errors to ignore. + public Converter(IMessaging messaging, int indentationAmount, IEnumerable errorsAsWarnings = null, IEnumerable ignoreErrors = null) + { + // workaround IDE0009 bug + /*this.ConvertElementMapping = new Dictionary>() + { + { Converter.FileElementName, this.ConvertFileElement }, + { Converter.ExePackageElementName, this.ConvertSuppressSignatureValidation }, + { Converter.MsiPackageElementName, this.ConvertSuppressSignatureValidation }, + { Converter.MspPackageElementName, this.ConvertSuppressSignatureValidation }, + { Converter.MsuPackageElementName, this.ConvertSuppressSignatureValidation }, + { Converter.PayloadElementName, this.ConvertSuppressSignatureValidation }, + { Converter.WixElementWithoutNamespaceName, this.ConvertWixElementWithoutNamespace }, + };*/ + this.ConvertElementMapping = new Dictionary>(); + this.ConvertElementMapping.Add(Converter.FileElementName, this.ConvertFileElement); + this.ConvertElementMapping.Add(Converter.ExePackageElementName, this.ConvertSuppressSignatureValidation); + this.ConvertElementMapping.Add(Converter.MsiPackageElementName, this.ConvertSuppressSignatureValidation); + this.ConvertElementMapping.Add(Converter.MspPackageElementName, this.ConvertSuppressSignatureValidation); + this.ConvertElementMapping.Add(Converter.MsuPackageElementName, this.ConvertSuppressSignatureValidation); + this.ConvertElementMapping.Add(Converter.PayloadElementName, this.ConvertSuppressSignatureValidation); + this.ConvertElementMapping.Add(Converter.WixElementWithoutNamespaceName, this.ConvertWixElementWithoutNamespace); + + this.Messaging = messaging; + + this.IndentationAmount = indentationAmount; + + this.ErrorsAsWarnings = new HashSet(this.YieldConverterTypes(errorsAsWarnings)); + + this.IgnoreErrors = new HashSet(this.YieldConverterTypes(ignoreErrors)); + } + + private int Errors { get; set; } + + private HashSet ErrorsAsWarnings { get; set; } + + private HashSet IgnoreErrors { get; set; } + + private IMessaging Messaging { get; } + + private int IndentationAmount { get; set; } + + private string SourceFile { get; set; } + + /// + /// Convert a file. + /// + /// The file to convert. + /// Option to save the converted errors that are found. + /// The number of errors found. + public int ConvertFile(string sourceFile, bool saveConvertedFile) + { + XDocument document; + + // Set the instance info. + this.Errors = 0; + this.SourceFile = sourceFile; + + try + { + document = XDocument.Load(this.SourceFile, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo); + } + catch (XmlException e) + { + this.OnError(ConverterTestType.XmlException, (XObject)null, "The xml is invalid. Detail: '{0}'", e.Message); + + return this.Errors; + } + + this.ConvertDocument(document); + + // Fix errors if requested and necessary. + if (saveConvertedFile && 0 < this.Errors) + { + try + { + using (StreamWriter writer = File.CreateText(this.SourceFile)) + { + document.Save(writer, SaveOptions.DisableFormatting | SaveOptions.OmitDuplicateNamespaces); + } + } + catch (UnauthorizedAccessException) + { + this.OnError(ConverterTestType.UnauthorizedAccessException, (XObject)null, "Could not write to file."); + } + } + + return this.Errors; + } + + /// + /// Convert a document. + /// + /// The document to convert. + /// The number of errors found. + public int ConvertDocument(XDocument document) + { + XDeclaration declaration = document.Declaration; + + // Convert the declaration. + if (null != declaration) + { + if (!String.Equals("utf-8", declaration.Encoding, StringComparison.OrdinalIgnoreCase)) + { + if (this.OnError(ConverterTestType.DeclarationEncodingWrong, document.Root, "The XML declaration encoding is not properly set to 'utf-8'.")) + { + declaration.Encoding = "utf-8"; + } + } + } + else // missing declaration + { + if (this.OnError(ConverterTestType.DeclarationMissing, (XNode)null, "This file is missing an XML declaration on the first line.")) + { + document.Declaration = new XDeclaration("1.0", "utf-8", null); + document.Root.AddBeforeSelf(new XText(XDocumentNewLine)); + } + } + + // Start converting the nodes at the top. + this.ConvertNode(document.Root, 0); + + return this.Errors; + } + + /// + /// Convert a single xml node. + /// + /// The node to convert. + /// The depth level of the node. + /// The converted node. + private void ConvertNode(XNode node, int level) + { + // Convert this node's whitespace. + if ((XmlNodeType.Comment == node.NodeType && 0 > ((XComment)node).Value.IndexOf(XDocumentNewLine, StringComparison.Ordinal)) || + XmlNodeType.CDATA == node.NodeType || XmlNodeType.Element == node.NodeType || XmlNodeType.ProcessingInstruction == node.NodeType) + { + this.ConvertWhitespace(node, level); + } + + // Convert this node if it is an element. + XElement element = node as XElement; + + if (null != element) + { + this.ConvertElement(element); + + // Convert all children of this element. + IEnumerable children = element.Nodes().ToList(); + + foreach (XNode child in children) + { + this.ConvertNode(child, level + 1); + } + } + } + + private void ConvertElement(XElement element) + { + // Gather any deprecated namespaces, then update this element tree based on those deprecations. + Dictionary deprecatedToUpdatedNamespaces = new Dictionary(); + + foreach (XAttribute declaration in element.Attributes().Where(a => a.IsNamespaceDeclaration)) + { + XNamespace ns; + + if (Converter.OldToNewNamespaceMapping.TryGetValue(declaration.Value, out ns)) + { + if (this.OnError(ConverterTestType.XmlnsValueWrong, declaration, "The namespace '{0}' is out of date. It must be '{1}'.", declaration.Value, ns.NamespaceName)) + { + deprecatedToUpdatedNamespaces.Add(declaration.Value, ns); + } + } + } + + if (deprecatedToUpdatedNamespaces.Any()) + { + Converter.UpdateElementsWithDeprecatedNamespaces(element.DescendantsAndSelf(), deprecatedToUpdatedNamespaces); + } + + // Convert the node in much greater detail. + Action convert; + + if (this.ConvertElementMapping.TryGetValue(element.Name, out convert)) + { + convert(element); + } + } + + private void ConvertFileElement(XElement element) + { + if (null == element.Attribute("Id")) + { + XAttribute attribute = element.Attribute("Name"); + + if (null == attribute) + { + attribute = element.Attribute("Source"); + } + + if (null != attribute) + { + string name = Path.GetFileName(attribute.Value); + + if (this.OnError(ConverterTestType.AssignAnonymousFileId, element, "The file id is being updated to '{0}' to ensure it remains the same as the default", name)) + { + IEnumerable attributes = element.Attributes().ToList(); + element.RemoveAttributes(); + element.Add(new XAttribute("Id", ToolsCommon.GetIdentifierFromName(name))); + element.Add(attributes); + } + } + } + } + + private void ConvertSuppressSignatureValidation(XElement element) + { + XAttribute suppressSignatureValidation = element.Attribute("SuppressSignatureValidation"); + + if (null != suppressSignatureValidation) + { + if (this.OnError(ConverterTestType.SuppressSignatureValidationDeprecated, element, "The chain package element contains deprecated '{0}' attribute. Use the 'EnableSignatureValidation' instead.", suppressSignatureValidation)) + { + if ("no" == suppressSignatureValidation.Value) + { + element.Add(new XAttribute("EnableSignatureValidation", "yes")); + } + } + + suppressSignatureValidation.Remove(); + } + } + + /// + /// Converts a Wix element. + /// + /// The Wix element to convert. + /// The converted element. + private void ConvertWixElementWithoutNamespace(XElement element) + { + if (this.OnError(ConverterTestType.XmlnsMissing, element, "The xmlns attribute is missing. It must be present with a value of '{0}'.", WixNamespace.NamespaceName)) + { + element.Name = WixNamespace.GetName(element.Name.LocalName); + + element.Add(new XAttribute("xmlns", WixNamespace.NamespaceName)); // set the default namespace. + + foreach (XElement elementWithoutNamespace in element.Elements().Where(e => XNamespace.None == e.Name.Namespace)) + { + elementWithoutNamespace.Name = WixNamespace.GetName(elementWithoutNamespace.Name.LocalName); + } + } + } + + /// + /// Convert the whitespace adjacent to a node. + /// + /// The node to convert. + /// The depth level of the node. + private void ConvertWhitespace(XNode node, int level) + { + // Fix the whitespace before this node. + XText whitespace = node.PreviousNode as XText; + + if (null != whitespace) + { + if (XmlNodeType.CDATA == node.NodeType) + { + if (this.OnError(ConverterTestType.WhitespacePrecedingCDATAWrong, node, "There should be no whitespace preceding a CDATA node.")) + { + whitespace.Remove(); + } + } + else + { + // TODO: this code complains about whitespace even after the file has been fixed. + if (!Converter.IsLegalWhitespace(this.IndentationAmount, level, whitespace.Value)) + { + if (this.OnError(ConverterTestType.WhitespacePrecedingNodeWrong, node, "The whitespace preceding this node is incorrect.")) + { + Converter.FixWhitespace(this.IndentationAmount, level, whitespace); + } + } + } + } + + // Fix the whitespace after CDATA nodes. + XCData cdata = node as XCData; + + if (null != cdata) + { + whitespace = cdata.NextNode as XText; + + if (null != whitespace) + { + if (this.OnError(ConverterTestType.WhitespaceFollowingCDATAWrong, node, "There should be no whitespace following a CDATA node.")) + { + whitespace.Remove(); + } + } + } + else + { + // Fix the whitespace inside and after this node (except for Error which may contain just whitespace). + XElement element = node as XElement; + + if (null != element && "Error" != element.Name.LocalName) + { + if (!element.HasElements && !element.IsEmpty && String.IsNullOrEmpty(element.Value.Trim())) + { + if (this.OnError(ConverterTestType.NotEmptyElement, element, "This should be an empty element since it contains nothing but whitespace.")) + { + element.RemoveNodes(); + } + } + + whitespace = node.NextNode as XText; + + if (null != whitespace) + { + // TODO: this code crashes when level is 0, + // complains about whitespace even after the file has been fixed, + // and the error text doesn't match the error. + if (!Converter.IsLegalWhitespace(this.IndentationAmount, level - 1, whitespace.Value)) + { + if (this.OnError(ConverterTestType.WhitespacePrecedingEndElementWrong, whitespace, "The whitespace preceding this end element is incorrect.")) + { + Converter.FixWhitespace(this.IndentationAmount, level - 1, whitespace); + } + } + } + } + } + } + + private IEnumerable YieldConverterTypes(IEnumerable types) + { + if (null != types) + { + foreach (string type in types) + { + ConverterTestType itt; + + if (Enum.TryParse(type, true, out itt)) + { + yield return itt; + } + else // not a known ConverterTestType + { + this.OnError(ConverterTestType.ConverterTestTypeUnknown, (XObject)null, "Unknown error type: '{0}'.", type); + } + } + } + } + + private static void UpdateElementsWithDeprecatedNamespaces(IEnumerable elements, Dictionary deprecatedToUpdatedNamespaces) + { + foreach (XElement element in elements) + { + XNamespace ns; + + if (deprecatedToUpdatedNamespaces.TryGetValue(element.Name.Namespace, out ns)) + { + element.Name = ns.GetName(element.Name.LocalName); + } + + // Remove all the attributes and add them back to with their namespace updated (as necessary). + IEnumerable attributes = element.Attributes().ToList(); + element.RemoveAttributes(); + + foreach (XAttribute attribute in attributes) + { + XAttribute convertedAttribute = attribute; + + if (attribute.IsNamespaceDeclaration) + { + if (deprecatedToUpdatedNamespaces.TryGetValue(attribute.Value, out ns)) + { + convertedAttribute = ("xmlns" == attribute.Name.LocalName) ? new XAttribute(attribute.Name.LocalName, ns.NamespaceName) : new XAttribute(XNamespace.Xmlns + attribute.Name.LocalName, ns.NamespaceName); + } + } + else if (deprecatedToUpdatedNamespaces.TryGetValue(attribute.Name.Namespace, out ns)) + { + convertedAttribute = new XAttribute(ns.GetName(attribute.Name.LocalName), attribute.Value); + } + + element.Add(convertedAttribute); + } + } + } + + /// + /// Determine if the whitespace preceding a node is appropriate for its depth level. + /// + /// Indentation value to use when validating leading whitespace. + /// The depth level that should match this whitespace. + /// The whitespace to validate. + /// true if the whitespace is legal; false otherwise. + private static bool IsLegalWhitespace(int indentationAmount, int level, string whitespace) + { + // strip off leading newlines; there can be an arbitrary number of these + while (whitespace.StartsWith(XDocumentNewLine, StringComparison.Ordinal)) + { + whitespace = whitespace.Substring(XDocumentNewLine.Length); + } + + // check the length + if (whitespace.Length != level * indentationAmount) + { + return false; + } + + // check the spaces + foreach (char character in whitespace) + { + if (' ' != character) + { + return false; + } + } + + return true; + } + + /// + /// Fix the whitespace in a Whitespace node. + /// + /// Indentation value to use when validating leading whitespace. + /// The depth level of the desired whitespace. + /// The whitespace node to fix. + private static void FixWhitespace(int indentationAmount, int level, XText whitespace) + { + int newLineCount = 0; + + for (int i = 0; i + 1 < whitespace.Value.Length; ++i) + { + if (XDocumentNewLine == whitespace.Value.Substring(i, 2)) + { + ++i; // skip an extra character + ++newLineCount; + } + } + + if (0 == newLineCount) + { + newLineCount = 1; + } + + // reset the whitespace value + whitespace.Value = String.Empty; + + // add the correct number of newlines + for (int i = 0; i < newLineCount; ++i) + { + whitespace.Value = String.Concat(whitespace.Value, XDocumentNewLine); + } + + // add the correct number of spaces based on configured indentation amount + whitespace.Value = String.Concat(whitespace.Value, new string(' ', level * indentationAmount)); + } + + /// + /// Output an error message to the console. + /// + /// The type of converter test. + /// The node that caused the error. + /// Detailed error message. + /// Additional formatted string arguments. + /// Returns true indicating that action should be taken on this error, and false if it should be ignored. + private bool OnError(ConverterTestType converterTestType, XObject node, string message, params object[] args) + { + if (this.IgnoreErrors.Contains(converterTestType)) // ignore the error + { + return false; + } + + // Increase the error count. + this.Errors++; + + SourceLineNumber sourceLine = (null == node) ? new SourceLineNumber(this.SourceFile ?? "wixcop.exe") : new SourceLineNumber(this.SourceFile, ((IXmlLineInfo)node).LineNumber); + bool warning = this.ErrorsAsWarnings.Contains(converterTestType); + string display = String.Format(CultureInfo.CurrentCulture, message, args); + + var msg = new Message(sourceLine, warning ? MessageLevel.Warning : MessageLevel.Error, (int)converterTestType, "{0} ({1})", display, converterTestType.ToString()); + + this.Messaging.Write(msg); + + return true; + } + + /// + /// Converter test types. These are used to condition error messages down to warnings. + /// + private enum ConverterTestType + { + /// + /// Internal-only: displayed when a string cannot be converted to an ConverterTestType. + /// + ConverterTestTypeUnknown, + + /// + /// Displayed when an XML loading exception has occurred. + /// + XmlException, + + /// + /// Displayed when a file cannot be accessed; typically when trying to save back a fixed file. + /// + UnauthorizedAccessException, + + /// + /// Displayed when the encoding attribute in the XML declaration is not 'UTF-8'. + /// + DeclarationEncodingWrong, + + /// + /// Displayed when the XML declaration is missing from the source file. + /// + DeclarationMissing, + + /// + /// Displayed when the whitespace preceding a CDATA node is wrong. + /// + WhitespacePrecedingCDATAWrong, + + /// + /// Displayed when the whitespace preceding a node is wrong. + /// + WhitespacePrecedingNodeWrong, + + /// + /// Displayed when an element is not empty as it should be. + /// + NotEmptyElement, + + /// + /// Displayed when the whitespace following a CDATA node is wrong. + /// + WhitespaceFollowingCDATAWrong, + + /// + /// Displayed when the whitespace preceding an end element is wrong. + /// + WhitespacePrecedingEndElementWrong, + + /// + /// Displayed when the xmlns attribute is missing from the document element. + /// + XmlnsMissing, + + /// + /// Displayed when the xmlns attribute on the document element is wrong. + /// + XmlnsValueWrong, + + /// + /// Assign an identifier to a File element when on Id attribute is specified. + /// + AssignAnonymousFileId, + + /// + /// SuppressSignatureValidation attribute is deprecated and replaced with EnableSignatureValidation. + /// + SuppressSignatureValidationDeprecated, + } + } +} diff --git a/src/wixcop/Interfaces/IWixCopCommandLineParser.cs b/src/wixcop/Interfaces/IWixCopCommandLineParser.cs new file mode 100644 index 00000000..2093f5d8 --- /dev/null +++ b/src/wixcop/Interfaces/IWixCopCommandLineParser.cs @@ -0,0 +1,11 @@ +using WixToolset.Extensibility.Data; + +namespace WixCop.Interfaces +{ + public interface IWixCopCommandLineParser + { + ICommandLineArguments Arguments { get; set; } + + ICommandLineCommand ParseWixCopCommandLine(); + } +} diff --git a/src/wixcop/Program.cs b/src/wixcop/Program.cs new file mode 100644 index 00000000..b26bd6c9 --- /dev/null +++ b/src/wixcop/Program.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixCop +{ + using System; + using WixCop.CommandLine; + using WixCop.Interfaces; + using WixToolset.Core; + using WixToolset.Extensibility; + using WixToolset.Extensibility.Data; + using WixToolset.Extensibility.Services; + using WixToolset.Tools.Core; + + /// + /// Wix source code style inspector and converter. + /// + public sealed class Program + { + /// + /// The main entry point for the application. + /// + /// The commandline arguments. + /// The number of errors that were found. + [STAThread] + public static int Main(string[] args) + { + var serviceProvider = new WixToolsetServiceProvider(); + var listener = new ConsoleMessageListener("WXCP", "wixcop.exe"); + + serviceProvider.AddService((x, y) => listener); + serviceProvider.AddService((x, y) => new WixCopCommandLineParser(x)); + + var program = new Program(); + return program.Run(serviceProvider, args); + } + + /// + /// Run the application with the given arguments. + /// + /// Service provider to use throughout this execution. + /// The commandline arguments. + /// The number of errors that were found. + public int Run(IServiceProvider serviceProvider, string[] args) + { + try + { + var listener = serviceProvider.GetService(); + var messaging = serviceProvider.GetService(); + messaging.SetListener(listener); + + var arguments = serviceProvider.GetService(); + arguments.Populate(args); + + var commandLine = serviceProvider.GetService(); + commandLine.Arguments = arguments; + var command = commandLine.ParseWixCopCommandLine(); + return command?.Execute() ?? 1; + } + catch (Exception e) + { + Console.Error.WriteLine("wixcop.exe : fatal error WXCP0001 : {0}\r\n\n\nStack Trace:\r\n{1}", e.Message, e.StackTrace); + + return 1; + } + } + } +} diff --git a/src/wixcop/WixCop.csproj b/src/wixcop/WixCop.csproj new file mode 100644 index 00000000..9bcae177 --- /dev/null +++ b/src/wixcop/WixCop.csproj @@ -0,0 +1,32 @@ + + + + + + net461;netcoreapp2.1 + Exe + Converter + WiX Error Correction Tool + embedded + true + + + + + NU1701 + + + + + + + + + + + + + + + + -- cgit v1.2.3-55-g6feb