aboutsummaryrefslogtreecommitdiff
path: root/src/wixcop
diff options
context:
space:
mode:
Diffstat (limited to 'src/wixcop')
-rw-r--r--src/wixcop/CommandLine/ConvertCommand.cs212
-rw-r--r--src/wixcop/CommandLine/HelpCommand.cs25
-rw-r--r--src/wixcop/CommandLine/WixCopCommandLineParser.cs132
-rw-r--r--src/wixcop/Converter.cs633
-rw-r--r--src/wixcop/Interfaces/IWixCopCommandLineParser.cs11
-rw-r--r--src/wixcop/Program.cs67
-rw-r--r--src/wixcop/WixCop.csproj32
7 files changed, 1112 insertions, 0 deletions
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 @@
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Linq;
5using System.Xml;
6using WixToolset.Extensibility.Data;
7using WixToolset.Extensibility.Services;
8
9namespace WixCop.CommandLine
10{
11 internal class ConvertCommand : ICommandLineCommand
12 {
13 private const string SettingsFileDefault = "wixcop.settings.xml";
14
15 public ConvertCommand(IServiceProvider serviceProvider, bool fixErrors, int indentationAmount, List<string> searchPatterns, bool subDirectories, string settingsFile1, string settingsFile2)
16 {
17 this.ErrorsAsWarnings = new HashSet<string>();
18 this.ExemptFiles = new HashSet<string>();
19 this.FixErrors = fixErrors;
20 this.IndentationAmount = indentationAmount;
21 this.IgnoreErrors = new HashSet<string>();
22 this.SearchPatternResults = new HashSet<string>();
23 this.SearchPatterns = searchPatterns;
24 this.ServiceProvider = serviceProvider;
25 this.SettingsFile1 = settingsFile1;
26 this.SettingsFile2 = settingsFile2;
27 this.SubDirectories = subDirectories;
28 }
29
30 private HashSet<string> ErrorsAsWarnings { get; }
31
32 private HashSet<string> ExemptFiles { get; }
33
34 private bool FixErrors { get; }
35
36 private int IndentationAmount { get; }
37
38 private HashSet<string> IgnoreErrors { get; }
39
40 private HashSet<string> SearchPatternResults { get; }
41
42 private List<string> SearchPatterns { get; }
43
44 private IServiceProvider ServiceProvider { get; }
45
46 private string SettingsFile1 { get; }
47
48 private string SettingsFile2 { get; }
49
50 private bool SubDirectories { get; }
51
52 public int Execute()
53 {
54 // parse the settings if any were specified
55 if (null != this.SettingsFile1 || null != this.SettingsFile2)
56 {
57 this.ParseSettingsFiles(this.SettingsFile1, this.SettingsFile2);
58 }
59 else
60 {
61 if (File.Exists(ConvertCommand.SettingsFileDefault))
62 {
63 this.ParseSettingsFiles(ConvertCommand.SettingsFileDefault, null);
64 }
65 }
66
67 var messaging = this.ServiceProvider.GetService<IMessaging>();
68 var converter = new Converter(messaging, this.IndentationAmount, this.ErrorsAsWarnings, this.IgnoreErrors);
69
70 var errors = this.InspectSubDirectories(converter, Path.GetFullPath("."));
71
72 foreach (string searchPattern in this.SearchPatterns)
73 {
74 if (!this.SearchPatternResults.Contains(searchPattern))
75 {
76 Console.Error.WriteLine("Could not find file \"{0}\"", searchPattern);
77 errors++;
78 }
79 }
80
81 return errors != 0 ? 2 : 0;
82 }
83
84 /// <summary>
85 /// Get the files that match a search path pattern.
86 /// </summary>
87 /// <param name="baseDir">The base directory at which to begin the search.</param>
88 /// <param name="searchPath">The search path pattern.</param>
89 /// <returns>The files matching the pattern.</returns>
90 private static string[] GetFiles(string baseDir, string searchPath)
91 {
92 // convert alternate directory separators to the standard one
93 var filePath = searchPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
94 var lastSeparator = filePath.LastIndexOf(Path.DirectorySeparatorChar);
95 string[] files = null;
96
97 try
98 {
99 if (0 > lastSeparator)
100 {
101 files = Directory.GetFiles(baseDir, filePath);
102 }
103 else // found directory separator
104 {
105 var searchPattern = filePath.Substring(lastSeparator + 1);
106
107 files = Directory.GetFiles(filePath.Substring(0, lastSeparator + 1), searchPattern);
108 }
109 }
110 catch (DirectoryNotFoundException)
111 {
112 // don't let this function throw the DirectoryNotFoundException. (this exception
113 // occurs for non-existant directories and invalid characters in the searchPattern)
114 }
115
116 return files;
117 }
118
119 /// <summary>
120 /// Inspect sub-directories.
121 /// </summary>
122 /// <param name="directory">The directory whose sub-directories will be inspected.</param>
123 /// <returns>The number of errors that were found.</returns>
124 private int InspectSubDirectories(Converter converter, string directory)
125 {
126 var errors = 0;
127
128 foreach (var searchPattern in this.SearchPatterns)
129 {
130 foreach (var sourceFilePath in GetFiles(directory, searchPattern))
131 {
132 var file = new FileInfo(sourceFilePath);
133
134 if (!this.ExemptFiles.Contains(file.Name.ToUpperInvariant()))
135 {
136 this.SearchPatternResults.Add(searchPattern);
137 errors += converter.ConvertFile(file.FullName, this.FixErrors);
138 }
139 }
140 }
141
142 if (this.SubDirectories)
143 {
144 foreach (var childDirectoryPath in Directory.GetDirectories(directory))
145 {
146 errors += this.InspectSubDirectories(converter, childDirectoryPath);
147 }
148 }
149
150 return errors;
151 }
152
153 /// <summary>
154 /// Parse the primary and secondary settings files.
155 /// </summary>
156 /// <param name="localSettingsFile1">The primary settings file.</param>
157 /// <param name="localSettingsFile2">The secondary settings file.</param>
158 private void ParseSettingsFiles(string localSettingsFile1, string localSettingsFile2)
159 {
160 if (null == localSettingsFile1 && null != localSettingsFile2)
161 {
162 throw new ArgumentException("Cannot specify a secondary settings file (set2) without a primary settings file (set1).", "localSettingsFile2");
163 }
164
165 var settingsFile = localSettingsFile1;
166 while (null != settingsFile)
167 {
168 XmlTextReader reader = null;
169 try
170 {
171 reader = new XmlTextReader(settingsFile);
172 var doc = new XmlDocument();
173 doc.Load(reader);
174
175 // get the types of tests that will have their errors displayed as warnings
176 var testsIgnoredElements = doc.SelectNodes("/Settings/IgnoreErrors/Test");
177 foreach (XmlElement test in testsIgnoredElements)
178 {
179 var key = test.GetAttribute("Id");
180 this.IgnoreErrors.Add(key);
181 }
182
183 // get the types of tests that will have their errors displayed as warnings
184 var testsAsWarningsElements = doc.SelectNodes("/Settings/ErrorsAsWarnings/Test");
185 foreach (XmlElement test in testsAsWarningsElements)
186 {
187 var key = test.GetAttribute("Id");
188 this.ErrorsAsWarnings.Add(key);
189 }
190
191 // get the exempt files
192 var localExemptFiles = doc.SelectNodes("/Settings/ExemptFiles/File");
193 foreach (XmlElement file in localExemptFiles)
194 {
195 var key = file.GetAttribute("Name").ToUpperInvariant();
196 this.ExemptFiles.Add(key);
197 }
198 }
199 finally
200 {
201 if (null != reader)
202 {
203 reader.Close();
204 }
205 }
206
207 settingsFile = localSettingsFile2;
208 localSettingsFile2 = null;
209 }
210 }
211 }
212}
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 @@
1using System;
2using WixToolset.Extensibility.Data;
3
4namespace WixCop.CommandLine
5{
6 internal class HelpCommand : ICommandLineCommand
7 {
8 public int Execute()
9 {
10 Console.WriteLine(" usage: wixcop.exe sourceFile [sourceFile ...]");
11 Console.WriteLine();
12 Console.WriteLine(" -f fix errors automatically for writable files");
13 Console.WriteLine(" -nologo suppress displaying the logo information");
14 Console.WriteLine(" -s search for matching files in current dir and subdirs");
15 Console.WriteLine(" -set1<file> primary settings file");
16 Console.WriteLine(" -set2<file> secondary settings file (overrides primary)");
17 Console.WriteLine(" -indent:<n> indentation multiple (overrides default of 4)");
18 Console.WriteLine(" -? this help information");
19 Console.WriteLine();
20 Console.WriteLine(" sourceFile may use wildcards like *.wxs");
21
22 return 0;
23 }
24 }
25}
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 @@
1using System;
2using System.Collections.Generic;
3using WixCop.Interfaces;
4using WixToolset.Core;
5using WixToolset.Extensibility.Data;
6using WixToolset.Extensibility.Services;
7
8namespace WixCop.CommandLine
9{
10 public sealed class WixCopCommandLineParser : IWixCopCommandLineParser
11 {
12 private bool fixErrors;
13 private int indentationAmount;
14 private readonly List<string> searchPatterns;
15 private readonly IServiceProvider serviceProvider;
16 private string settingsFile1;
17 private string settingsFile2;
18 private bool showHelp;
19 private bool showLogo;
20 private bool subDirectories;
21
22 public WixCopCommandLineParser(IServiceProvider serviceProvider)
23 {
24 this.serviceProvider = serviceProvider;
25
26 this.indentationAmount = 4;
27 this.searchPatterns = new List<string>();
28 this.showLogo = true;
29 }
30
31 public ICommandLineArguments Arguments { get; set; }
32
33 public ICommandLineCommand ParseWixCopCommandLine()
34 {
35 this.Parse();
36
37 if (this.showLogo)
38 {
39 AppCommon.DisplayToolHeader();
40 Console.WriteLine();
41 }
42
43 if (this.showHelp)
44 {
45 return new HelpCommand();
46 }
47
48 return new ConvertCommand(
49 this.serviceProvider,
50 this.fixErrors,
51 this.indentationAmount,
52 this.searchPatterns,
53 this.subDirectories,
54 this.settingsFile1,
55 this.settingsFile2);
56 }
57
58 private void Parse()
59 {
60 this.showHelp = 0 == this.Arguments.Arguments.Length;
61 var parser = this.Arguments.Parse();
62
63 while (!this.showHelp &&
64 String.IsNullOrEmpty(parser.ErrorArgument) &&
65 parser.TryGetNextSwitchOrArgument(out var arg))
66 {
67 if (String.IsNullOrWhiteSpace(arg)) // skip blank arguments.
68 {
69 continue;
70 }
71
72 if (parser.IsSwitch(arg))
73 {
74 if (!this.ParseArgument(parser, arg))
75 {
76 parser.ErrorArgument = arg;
77 }
78 }
79 else
80 {
81 this.searchPatterns.Add(arg);
82 }
83 }
84 }
85
86 private bool ParseArgument(IParseCommandLine parser, string arg)
87 {
88 var parameter = arg.Substring(1);
89
90 switch (parameter.ToLowerInvariant())
91 {
92 case "?":
93 this.showHelp = true;
94 return true;
95 case "f":
96 this.fixErrors = true;
97 return true;
98 case "nologo":
99 this.showLogo = false;
100 return true;
101 case "s":
102 this.subDirectories = true;
103 return true;
104 default: // other parameters
105 if (parameter.StartsWith("set1", StringComparison.Ordinal))
106 {
107 this.settingsFile1 = parameter.Substring(4);
108 }
109 else if (parameter.StartsWith("set2", StringComparison.Ordinal))
110 {
111 this.settingsFile2 = parameter.Substring(4);
112 }
113 else if (parameter.StartsWith("indent:", StringComparison.Ordinal))
114 {
115 try
116 {
117 this.indentationAmount = Convert.ToInt32(parameter.Substring(7));
118 }
119 catch
120 {
121 throw new ArgumentException("Invalid numeric argument.", parameter);
122 }
123 }
124 else
125 {
126 throw new ArgumentException("Invalid argument.", parameter);
127 }
128 return true;
129 }
130 }
131 }
132}
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 @@
1// 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.
2
3namespace WixCop
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.IO;
9 using System.Linq;
10 using System.Xml;
11 using System.Xml.Linq;
12 using WixToolset.Data;
13 using WixToolset.Extensibility.Services;
14 using WixToolset.Tools.Core;
15
16 /// <summary>
17 /// WiX source code converter.
18 /// </summary>
19 public class Converter
20 {
21 private const string XDocumentNewLine = "\n"; // XDocument normlizes "\r\n" to just "\n".
22 private static readonly XNamespace WixNamespace = "http://wixtoolset.org/schemas/v4/wxs";
23
24 private static readonly XName FileElementName = WixNamespace + "File";
25 private static readonly XName ExePackageElementName = WixNamespace + "ExePackage";
26 private static readonly XName MsiPackageElementName = WixNamespace + "MsiPackage";
27 private static readonly XName MspPackageElementName = WixNamespace + "MspPackage";
28 private static readonly XName MsuPackageElementName = WixNamespace + "MsuPackage";
29 private static readonly XName PayloadElementName = WixNamespace + "Payload";
30 private static readonly XName WixElementWithoutNamespaceName = XNamespace.None + "Wix";
31
32 private static readonly Dictionary<string, XNamespace> OldToNewNamespaceMapping = new Dictionary<string, XNamespace>()
33 {
34 { "http://schemas.microsoft.com/wix/BalExtension", "http://wixtoolset.org/schemas/v4/wxs/bal" },
35 { "http://schemas.microsoft.com/wix/ComPlusExtension", "http://wixtoolset.org/schemas/v4/wxs/complus" },
36 { "http://schemas.microsoft.com/wix/DependencyExtension", "http://wixtoolset.org/schemas/v4/wxs/dependency" },
37 { "http://schemas.microsoft.com/wix/DifxAppExtension", "http://wixtoolset.org/schemas/v4/wxs/difxapp" },
38 { "http://schemas.microsoft.com/wix/FirewallExtension", "http://wixtoolset.org/schemas/v4/wxs/firewall" },
39 { "http://schemas.microsoft.com/wix/GamingExtension", "http://wixtoolset.org/schemas/v4/wxs/gaming" },
40 { "http://schemas.microsoft.com/wix/IIsExtension", "http://wixtoolset.org/schemas/v4/wxs/iis" },
41 { "http://schemas.microsoft.com/wix/MsmqExtension", "http://wixtoolset.org/schemas/v4/wxs/msmq" },
42 { "http://schemas.microsoft.com/wix/NetFxExtension", "http://wixtoolset.org/schemas/v4/wxs/netfx" },
43 { "http://schemas.microsoft.com/wix/PSExtension", "http://wixtoolset.org/schemas/v4/wxs/powershell" },
44 { "http://schemas.microsoft.com/wix/SqlExtension", "http://wixtoolset.org/schemas/v4/wxs/sql" },
45 { "http://schemas.microsoft.com/wix/TagExtension", "http://wixtoolset.org/schemas/v4/wxs/tag" },
46 { "http://schemas.microsoft.com/wix/UtilExtension", "http://wixtoolset.org/schemas/v4/wxs/util" },
47 { "http://schemas.microsoft.com/wix/VSExtension", "http://wixtoolset.org/schemas/v4/wxs/vs" },
48 { "http://wixtoolset.org/schemas/thmutil/2010", "http://wixtoolset.org/schemas/v4/thmutil" },
49 { "http://schemas.microsoft.com/wix/2009/Lux", "http://wixtoolset.org/schemas/v4/lux" },
50 { "http://schemas.microsoft.com/wix/2006/wi", "http://wixtoolset.org/schemas/v4/wxs" },
51 { "http://schemas.microsoft.com/wix/2006/localization", "http://wixtoolset.org/schemas/v4/wxl" },
52 { "http://schemas.microsoft.com/wix/2006/libraries", "http://wixtoolset.org/schemas/v4/wixlib" },
53 { "http://schemas.microsoft.com/wix/2006/objects", "http://wixtoolset.org/schemas/v4/wixobj" },
54 { "http://schemas.microsoft.com/wix/2006/outputs", "http://wixtoolset.org/schemas/v4/wixout" },
55 { "http://schemas.microsoft.com/wix/2007/pdbs", "http://wixtoolset.org/schemas/v4/wixpdb" },
56 { "http://schemas.microsoft.com/wix/2003/04/actions", "http://wixtoolset.org/schemas/v4/wi/actions" },
57 { "http://schemas.microsoft.com/wix/2006/tables", "http://wixtoolset.org/schemas/v4/wi/tables" },
58 { "http://schemas.microsoft.com/wix/2006/WixUnit", "http://wixtoolset.org/schemas/v4/wixunit" },
59 };
60
61 private Dictionary<XName, Action<XElement>> ConvertElementMapping;
62
63 /// <summary>
64 /// Instantiate a new Converter class.
65 /// </summary>
66 /// <param name="indentationAmount">Indentation value to use when validating leading whitespace.</param>
67 /// <param name="errorsAsWarnings">Test errors to display as warnings.</param>
68 /// <param name="ignoreErrors">Test errors to ignore.</param>
69 public Converter(IMessaging messaging, int indentationAmount, IEnumerable<string> errorsAsWarnings = null, IEnumerable<string> ignoreErrors = null)
70 {
71 // workaround IDE0009 bug
72 /*this.ConvertElementMapping = new Dictionary<XName, Action<XElement>>()
73 {
74 { Converter.FileElementName, this.ConvertFileElement },
75 { Converter.ExePackageElementName, this.ConvertSuppressSignatureValidation },
76 { Converter.MsiPackageElementName, this.ConvertSuppressSignatureValidation },
77 { Converter.MspPackageElementName, this.ConvertSuppressSignatureValidation },
78 { Converter.MsuPackageElementName, this.ConvertSuppressSignatureValidation },
79 { Converter.PayloadElementName, this.ConvertSuppressSignatureValidation },
80 { Converter.WixElementWithoutNamespaceName, this.ConvertWixElementWithoutNamespace },
81 };*/
82 this.ConvertElementMapping = new Dictionary<XName, Action<XElement>>();
83 this.ConvertElementMapping.Add(Converter.FileElementName, this.ConvertFileElement);
84 this.ConvertElementMapping.Add(Converter.ExePackageElementName, this.ConvertSuppressSignatureValidation);
85 this.ConvertElementMapping.Add(Converter.MsiPackageElementName, this.ConvertSuppressSignatureValidation);
86 this.ConvertElementMapping.Add(Converter.MspPackageElementName, this.ConvertSuppressSignatureValidation);
87 this.ConvertElementMapping.Add(Converter.MsuPackageElementName, this.ConvertSuppressSignatureValidation);
88 this.ConvertElementMapping.Add(Converter.PayloadElementName, this.ConvertSuppressSignatureValidation);
89 this.ConvertElementMapping.Add(Converter.WixElementWithoutNamespaceName, this.ConvertWixElementWithoutNamespace);
90
91 this.Messaging = messaging;
92
93 this.IndentationAmount = indentationAmount;
94
95 this.ErrorsAsWarnings = new HashSet<ConverterTestType>(this.YieldConverterTypes(errorsAsWarnings));
96
97 this.IgnoreErrors = new HashSet<ConverterTestType>(this.YieldConverterTypes(ignoreErrors));
98 }
99
100 private int Errors { get; set; }
101
102 private HashSet<ConverterTestType> ErrorsAsWarnings { get; set; }
103
104 private HashSet<ConverterTestType> IgnoreErrors { get; set; }
105
106 private IMessaging Messaging { get; }
107
108 private int IndentationAmount { get; set; }
109
110 private string SourceFile { get; set; }
111
112 /// <summary>
113 /// Convert a file.
114 /// </summary>
115 /// <param name="sourceFile">The file to convert.</param>
116 /// <param name="saveConvertedFile">Option to save the converted errors that are found.</param>
117 /// <returns>The number of errors found.</returns>
118 public int ConvertFile(string sourceFile, bool saveConvertedFile)
119 {
120 XDocument document;
121
122 // Set the instance info.
123 this.Errors = 0;
124 this.SourceFile = sourceFile;
125
126 try
127 {
128 document = XDocument.Load(this.SourceFile, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
129 }
130 catch (XmlException e)
131 {
132 this.OnError(ConverterTestType.XmlException, (XObject)null, "The xml is invalid. Detail: '{0}'", e.Message);
133
134 return this.Errors;
135 }
136
137 this.ConvertDocument(document);
138
139 // Fix errors if requested and necessary.
140 if (saveConvertedFile && 0 < this.Errors)
141 {
142 try
143 {
144 using (StreamWriter writer = File.CreateText(this.SourceFile))
145 {
146 document.Save(writer, SaveOptions.DisableFormatting | SaveOptions.OmitDuplicateNamespaces);
147 }
148 }
149 catch (UnauthorizedAccessException)
150 {
151 this.OnError(ConverterTestType.UnauthorizedAccessException, (XObject)null, "Could not write to file.");
152 }
153 }
154
155 return this.Errors;
156 }
157
158 /// <summary>
159 /// Convert a document.
160 /// </summary>
161 /// <param name="document">The document to convert.</param>
162 /// <returns>The number of errors found.</returns>
163 public int ConvertDocument(XDocument document)
164 {
165 XDeclaration declaration = document.Declaration;
166
167 // Convert the declaration.
168 if (null != declaration)
169 {
170 if (!String.Equals("utf-8", declaration.Encoding, StringComparison.OrdinalIgnoreCase))
171 {
172 if (this.OnError(ConverterTestType.DeclarationEncodingWrong, document.Root, "The XML declaration encoding is not properly set to 'utf-8'."))
173 {
174 declaration.Encoding = "utf-8";
175 }
176 }
177 }
178 else // missing declaration
179 {
180 if (this.OnError(ConverterTestType.DeclarationMissing, (XNode)null, "This file is missing an XML declaration on the first line."))
181 {
182 document.Declaration = new XDeclaration("1.0", "utf-8", null);
183 document.Root.AddBeforeSelf(new XText(XDocumentNewLine));
184 }
185 }
186
187 // Start converting the nodes at the top.
188 this.ConvertNode(document.Root, 0);
189
190 return this.Errors;
191 }
192
193 /// <summary>
194 /// Convert a single xml node.
195 /// </summary>
196 /// <param name="node">The node to convert.</param>
197 /// <param name="level">The depth level of the node.</param>
198 /// <returns>The converted node.</returns>
199 private void ConvertNode(XNode node, int level)
200 {
201 // Convert this node's whitespace.
202 if ((XmlNodeType.Comment == node.NodeType && 0 > ((XComment)node).Value.IndexOf(XDocumentNewLine, StringComparison.Ordinal)) ||
203 XmlNodeType.CDATA == node.NodeType || XmlNodeType.Element == node.NodeType || XmlNodeType.ProcessingInstruction == node.NodeType)
204 {
205 this.ConvertWhitespace(node, level);
206 }
207
208 // Convert this node if it is an element.
209 XElement element = node as XElement;
210
211 if (null != element)
212 {
213 this.ConvertElement(element);
214
215 // Convert all children of this element.
216 IEnumerable<XNode> children = element.Nodes().ToList();
217
218 foreach (XNode child in children)
219 {
220 this.ConvertNode(child, level + 1);
221 }
222 }
223 }
224
225 private void ConvertElement(XElement element)
226 {
227 // Gather any deprecated namespaces, then update this element tree based on those deprecations.
228 Dictionary<XNamespace, XNamespace> deprecatedToUpdatedNamespaces = new Dictionary<XNamespace, XNamespace>();
229
230 foreach (XAttribute declaration in element.Attributes().Where(a => a.IsNamespaceDeclaration))
231 {
232 XNamespace ns;
233
234 if (Converter.OldToNewNamespaceMapping.TryGetValue(declaration.Value, out ns))
235 {
236 if (this.OnError(ConverterTestType.XmlnsValueWrong, declaration, "The namespace '{0}' is out of date. It must be '{1}'.", declaration.Value, ns.NamespaceName))
237 {
238 deprecatedToUpdatedNamespaces.Add(declaration.Value, ns);
239 }
240 }
241 }
242
243 if (deprecatedToUpdatedNamespaces.Any())
244 {
245 Converter.UpdateElementsWithDeprecatedNamespaces(element.DescendantsAndSelf(), deprecatedToUpdatedNamespaces);
246 }
247
248 // Convert the node in much greater detail.
249 Action<XElement> convert;
250
251 if (this.ConvertElementMapping.TryGetValue(element.Name, out convert))
252 {
253 convert(element);
254 }
255 }
256
257 private void ConvertFileElement(XElement element)
258 {
259 if (null == element.Attribute("Id"))
260 {
261 XAttribute attribute = element.Attribute("Name");
262
263 if (null == attribute)
264 {
265 attribute = element.Attribute("Source");
266 }
267
268 if (null != attribute)
269 {
270 string name = Path.GetFileName(attribute.Value);
271
272 if (this.OnError(ConverterTestType.AssignAnonymousFileId, element, "The file id is being updated to '{0}' to ensure it remains the same as the default", name))
273 {
274 IEnumerable<XAttribute> attributes = element.Attributes().ToList();
275 element.RemoveAttributes();
276 element.Add(new XAttribute("Id", ToolsCommon.GetIdentifierFromName(name)));
277 element.Add(attributes);
278 }
279 }
280 }
281 }
282
283 private void ConvertSuppressSignatureValidation(XElement element)
284 {
285 XAttribute suppressSignatureValidation = element.Attribute("SuppressSignatureValidation");
286
287 if (null != suppressSignatureValidation)
288 {
289 if (this.OnError(ConverterTestType.SuppressSignatureValidationDeprecated, element, "The chain package element contains deprecated '{0}' attribute. Use the 'EnableSignatureValidation' instead.", suppressSignatureValidation))
290 {
291 if ("no" == suppressSignatureValidation.Value)
292 {
293 element.Add(new XAttribute("EnableSignatureValidation", "yes"));
294 }
295 }
296
297 suppressSignatureValidation.Remove();
298 }
299 }
300
301 /// <summary>
302 /// Converts a Wix element.
303 /// </summary>
304 /// <param name="element">The Wix element to convert.</param>
305 /// <returns>The converted element.</returns>
306 private void ConvertWixElementWithoutNamespace(XElement element)
307 {
308 if (this.OnError(ConverterTestType.XmlnsMissing, element, "The xmlns attribute is missing. It must be present with a value of '{0}'.", WixNamespace.NamespaceName))
309 {
310 element.Name = WixNamespace.GetName(element.Name.LocalName);
311
312 element.Add(new XAttribute("xmlns", WixNamespace.NamespaceName)); // set the default namespace.
313
314 foreach (XElement elementWithoutNamespace in element.Elements().Where(e => XNamespace.None == e.Name.Namespace))
315 {
316 elementWithoutNamespace.Name = WixNamespace.GetName(elementWithoutNamespace.Name.LocalName);
317 }
318 }
319 }
320
321 /// <summary>
322 /// Convert the whitespace adjacent to a node.
323 /// </summary>
324 /// <param name="node">The node to convert.</param>
325 /// <param name="level">The depth level of the node.</param>
326 private void ConvertWhitespace(XNode node, int level)
327 {
328 // Fix the whitespace before this node.
329 XText whitespace = node.PreviousNode as XText;
330
331 if (null != whitespace)
332 {
333 if (XmlNodeType.CDATA == node.NodeType)
334 {
335 if (this.OnError(ConverterTestType.WhitespacePrecedingCDATAWrong, node, "There should be no whitespace preceding a CDATA node."))
336 {
337 whitespace.Remove();
338 }
339 }
340 else
341 {
342 // TODO: this code complains about whitespace even after the file has been fixed.
343 if (!Converter.IsLegalWhitespace(this.IndentationAmount, level, whitespace.Value))
344 {
345 if (this.OnError(ConverterTestType.WhitespacePrecedingNodeWrong, node, "The whitespace preceding this node is incorrect."))
346 {
347 Converter.FixWhitespace(this.IndentationAmount, level, whitespace);
348 }
349 }
350 }
351 }
352
353 // Fix the whitespace after CDATA nodes.
354 XCData cdata = node as XCData;
355
356 if (null != cdata)
357 {
358 whitespace = cdata.NextNode as XText;
359
360 if (null != whitespace)
361 {
362 if (this.OnError(ConverterTestType.WhitespaceFollowingCDATAWrong, node, "There should be no whitespace following a CDATA node."))
363 {
364 whitespace.Remove();
365 }
366 }
367 }
368 else
369 {
370 // Fix the whitespace inside and after this node (except for Error which may contain just whitespace).
371 XElement element = node as XElement;
372
373 if (null != element && "Error" != element.Name.LocalName)
374 {
375 if (!element.HasElements && !element.IsEmpty && String.IsNullOrEmpty(element.Value.Trim()))
376 {
377 if (this.OnError(ConverterTestType.NotEmptyElement, element, "This should be an empty element since it contains nothing but whitespace."))
378 {
379 element.RemoveNodes();
380 }
381 }
382
383 whitespace = node.NextNode as XText;
384
385 if (null != whitespace)
386 {
387 // TODO: this code crashes when level is 0,
388 // complains about whitespace even after the file has been fixed,
389 // and the error text doesn't match the error.
390 if (!Converter.IsLegalWhitespace(this.IndentationAmount, level - 1, whitespace.Value))
391 {
392 if (this.OnError(ConverterTestType.WhitespacePrecedingEndElementWrong, whitespace, "The whitespace preceding this end element is incorrect."))
393 {
394 Converter.FixWhitespace(this.IndentationAmount, level - 1, whitespace);
395 }
396 }
397 }
398 }
399 }
400 }
401
402 private IEnumerable<ConverterTestType> YieldConverterTypes(IEnumerable<string> types)
403 {
404 if (null != types)
405 {
406 foreach (string type in types)
407 {
408 ConverterTestType itt;
409
410 if (Enum.TryParse<ConverterTestType>(type, true, out itt))
411 {
412 yield return itt;
413 }
414 else // not a known ConverterTestType
415 {
416 this.OnError(ConverterTestType.ConverterTestTypeUnknown, (XObject)null, "Unknown error type: '{0}'.", type);
417 }
418 }
419 }
420 }
421
422 private static void UpdateElementsWithDeprecatedNamespaces(IEnumerable<XElement> elements, Dictionary<XNamespace, XNamespace> deprecatedToUpdatedNamespaces)
423 {
424 foreach (XElement element in elements)
425 {
426 XNamespace ns;
427
428 if (deprecatedToUpdatedNamespaces.TryGetValue(element.Name.Namespace, out ns))
429 {
430 element.Name = ns.GetName(element.Name.LocalName);
431 }
432
433 // Remove all the attributes and add them back to with their namespace updated (as necessary).
434 IEnumerable<XAttribute> attributes = element.Attributes().ToList();
435 element.RemoveAttributes();
436
437 foreach (XAttribute attribute in attributes)
438 {
439 XAttribute convertedAttribute = attribute;
440
441 if (attribute.IsNamespaceDeclaration)
442 {
443 if (deprecatedToUpdatedNamespaces.TryGetValue(attribute.Value, out ns))
444 {
445 convertedAttribute = ("xmlns" == attribute.Name.LocalName) ? new XAttribute(attribute.Name.LocalName, ns.NamespaceName) : new XAttribute(XNamespace.Xmlns + attribute.Name.LocalName, ns.NamespaceName);
446 }
447 }
448 else if (deprecatedToUpdatedNamespaces.TryGetValue(attribute.Name.Namespace, out ns))
449 {
450 convertedAttribute = new XAttribute(ns.GetName(attribute.Name.LocalName), attribute.Value);
451 }
452
453 element.Add(convertedAttribute);
454 }
455 }
456 }
457
458 /// <summary>
459 /// Determine if the whitespace preceding a node is appropriate for its depth level.
460 /// </summary>
461 /// <param name="indentationAmount">Indentation value to use when validating leading whitespace.</param>
462 /// <param name="level">The depth level that should match this whitespace.</param>
463 /// <param name="whitespace">The whitespace to validate.</param>
464 /// <returns>true if the whitespace is legal; false otherwise.</returns>
465 private static bool IsLegalWhitespace(int indentationAmount, int level, string whitespace)
466 {
467 // strip off leading newlines; there can be an arbitrary number of these
468 while (whitespace.StartsWith(XDocumentNewLine, StringComparison.Ordinal))
469 {
470 whitespace = whitespace.Substring(XDocumentNewLine.Length);
471 }
472
473 // check the length
474 if (whitespace.Length != level * indentationAmount)
475 {
476 return false;
477 }
478
479 // check the spaces
480 foreach (char character in whitespace)
481 {
482 if (' ' != character)
483 {
484 return false;
485 }
486 }
487
488 return true;
489 }
490
491 /// <summary>
492 /// Fix the whitespace in a Whitespace node.
493 /// </summary>
494 /// <param name="indentationAmount">Indentation value to use when validating leading whitespace.</param>
495 /// <param name="level">The depth level of the desired whitespace.</param>
496 /// <param name="whitespace">The whitespace node to fix.</param>
497 private static void FixWhitespace(int indentationAmount, int level, XText whitespace)
498 {
499 int newLineCount = 0;
500
501 for (int i = 0; i + 1 < whitespace.Value.Length; ++i)
502 {
503 if (XDocumentNewLine == whitespace.Value.Substring(i, 2))
504 {
505 ++i; // skip an extra character
506 ++newLineCount;
507 }
508 }
509
510 if (0 == newLineCount)
511 {
512 newLineCount = 1;
513 }
514
515 // reset the whitespace value
516 whitespace.Value = String.Empty;
517
518 // add the correct number of newlines
519 for (int i = 0; i < newLineCount; ++i)
520 {
521 whitespace.Value = String.Concat(whitespace.Value, XDocumentNewLine);
522 }
523
524 // add the correct number of spaces based on configured indentation amount
525 whitespace.Value = String.Concat(whitespace.Value, new string(' ', level * indentationAmount));
526 }
527
528 /// <summary>
529 /// Output an error message to the console.
530 /// </summary>
531 /// <param name="converterTestType">The type of converter test.</param>
532 /// <param name="node">The node that caused the error.</param>
533 /// <param name="message">Detailed error message.</param>
534 /// <param name="args">Additional formatted string arguments.</param>
535 /// <returns>Returns true indicating that action should be taken on this error, and false if it should be ignored.</returns>
536 private bool OnError(ConverterTestType converterTestType, XObject node, string message, params object[] args)
537 {
538 if (this.IgnoreErrors.Contains(converterTestType)) // ignore the error
539 {
540 return false;
541 }
542
543 // Increase the error count.
544 this.Errors++;
545
546 SourceLineNumber sourceLine = (null == node) ? new SourceLineNumber(this.SourceFile ?? "wixcop.exe") : new SourceLineNumber(this.SourceFile, ((IXmlLineInfo)node).LineNumber);
547 bool warning = this.ErrorsAsWarnings.Contains(converterTestType);
548 string display = String.Format(CultureInfo.CurrentCulture, message, args);
549
550 var msg = new Message(sourceLine, warning ? MessageLevel.Warning : MessageLevel.Error, (int)converterTestType, "{0} ({1})", display, converterTestType.ToString());
551
552 this.Messaging.Write(msg);
553
554 return true;
555 }
556
557 /// <summary>
558 /// Converter test types. These are used to condition error messages down to warnings.
559 /// </summary>
560 private enum ConverterTestType
561 {
562 /// <summary>
563 /// Internal-only: displayed when a string cannot be converted to an ConverterTestType.
564 /// </summary>
565 ConverterTestTypeUnknown,
566
567 /// <summary>
568 /// Displayed when an XML loading exception has occurred.
569 /// </summary>
570 XmlException,
571
572 /// <summary>
573 /// Displayed when a file cannot be accessed; typically when trying to save back a fixed file.
574 /// </summary>
575 UnauthorizedAccessException,
576
577 /// <summary>
578 /// Displayed when the encoding attribute in the XML declaration is not 'UTF-8'.
579 /// </summary>
580 DeclarationEncodingWrong,
581
582 /// <summary>
583 /// Displayed when the XML declaration is missing from the source file.
584 /// </summary>
585 DeclarationMissing,
586
587 /// <summary>
588 /// Displayed when the whitespace preceding a CDATA node is wrong.
589 /// </summary>
590 WhitespacePrecedingCDATAWrong,
591
592 /// <summary>
593 /// Displayed when the whitespace preceding a node is wrong.
594 /// </summary>
595 WhitespacePrecedingNodeWrong,
596
597 /// <summary>
598 /// Displayed when an element is not empty as it should be.
599 /// </summary>
600 NotEmptyElement,
601
602 /// <summary>
603 /// Displayed when the whitespace following a CDATA node is wrong.
604 /// </summary>
605 WhitespaceFollowingCDATAWrong,
606
607 /// <summary>
608 /// Displayed when the whitespace preceding an end element is wrong.
609 /// </summary>
610 WhitespacePrecedingEndElementWrong,
611
612 /// <summary>
613 /// Displayed when the xmlns attribute is missing from the document element.
614 /// </summary>
615 XmlnsMissing,
616
617 /// <summary>
618 /// Displayed when the xmlns attribute on the document element is wrong.
619 /// </summary>
620 XmlnsValueWrong,
621
622 /// <summary>
623 /// Assign an identifier to a File element when on Id attribute is specified.
624 /// </summary>
625 AssignAnonymousFileId,
626
627 /// <summary>
628 /// SuppressSignatureValidation attribute is deprecated and replaced with EnableSignatureValidation.
629 /// </summary>
630 SuppressSignatureValidationDeprecated,
631 }
632 }
633}
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 @@
1using WixToolset.Extensibility.Data;
2
3namespace WixCop.Interfaces
4{
5 public interface IWixCopCommandLineParser
6 {
7 ICommandLineArguments Arguments { get; set; }
8
9 ICommandLineCommand ParseWixCopCommandLine();
10 }
11}
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 @@
1// 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.
2
3namespace WixCop
4{
5 using System;
6 using WixCop.CommandLine;
7 using WixCop.Interfaces;
8 using WixToolset.Core;
9 using WixToolset.Extensibility;
10 using WixToolset.Extensibility.Data;
11 using WixToolset.Extensibility.Services;
12 using WixToolset.Tools.Core;
13
14 /// <summary>
15 /// Wix source code style inspector and converter.
16 /// </summary>
17 public sealed class Program
18 {
19 /// <summary>
20 /// The main entry point for the application.
21 /// </summary>
22 /// <param name="args">The commandline arguments.</param>
23 /// <returns>The number of errors that were found.</returns>
24 [STAThread]
25 public static int Main(string[] args)
26 {
27 var serviceProvider = new WixToolsetServiceProvider();
28 var listener = new ConsoleMessageListener("WXCP", "wixcop.exe");
29
30 serviceProvider.AddService<IMessageListener>((x, y) => listener);
31 serviceProvider.AddService<IWixCopCommandLineParser>((x, y) => new WixCopCommandLineParser(x));
32
33 var program = new Program();
34 return program.Run(serviceProvider, args);
35 }
36
37 /// <summary>
38 /// Run the application with the given arguments.
39 /// </summary>
40 /// <param name="serviceProvider">Service provider to use throughout this execution.</param>
41 /// <param name="args">The commandline arguments.</param>
42 /// <returns>The number of errors that were found.</returns>
43 public int Run(IServiceProvider serviceProvider, string[] args)
44 {
45 try
46 {
47 var listener = serviceProvider.GetService<IMessageListener>();
48 var messaging = serviceProvider.GetService<IMessaging>();
49 messaging.SetListener(listener);
50
51 var arguments = serviceProvider.GetService<ICommandLineArguments>();
52 arguments.Populate(args);
53
54 var commandLine = serviceProvider.GetService<IWixCopCommandLineParser>();
55 commandLine.Arguments = arguments;
56 var command = commandLine.ParseWixCopCommandLine();
57 return command?.Execute() ?? 1;
58 }
59 catch (Exception e)
60 {
61 Console.Error.WriteLine("wixcop.exe : fatal error WXCP0001 : {0}\r\n\n\nStack Trace:\r\n{1}", e.Message, e.StackTrace);
62
63 return 1;
64 }
65 }
66 }
67}
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 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- 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. -->
3
4<Project Sdk="Microsoft.NET.Sdk">
5 <PropertyGroup>
6 <TargetFrameworks>net461;netcoreapp2.1</TargetFrameworks>
7 <OutputType>Exe</OutputType>
8 <Description>Converter</Description>
9 <Title>WiX Error Correction Tool</Title>
10 <DebugType>embedded</DebugType>
11 <PublishRepositoryUrl>true</PublishRepositoryUrl>
12 <!-- <PackAsTool>true</PackAsTool> -->
13 </PropertyGroup>
14
15 <PropertyGroup>
16 <NoWarn>NU1701</NoWarn>
17 </PropertyGroup>
18
19 <ItemGroup>
20 <ProjectReference Include="..\WixToolset.Tools.Core\WixToolset.Tools.Core.csproj" />
21 </ItemGroup>
22
23 <ItemGroup>
24 <ProjectReference Include="$(WixToolsetRootFolder)\Core\src\WixToolset.Core\WixToolset.Core.csproj" Condition=" '$(Configuration)' == 'Debug' And Exists('$(WixToolsetRootFolder)\Core\README.md') " />
25 <PackageReference Include="WixToolset.Core" Version="4.0.*" Condition=" '$(Configuration)' == 'Release' Or !Exists('$(WixToolsetRootFolder)\Core\README.md') " />
26 </ItemGroup>
27
28 <ItemGroup>
29 <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta-63102-01" PrivateAssets="All"/>
30 <PackageReference Include="Nerdbank.GitVersioning" Version="2.1.65" PrivateAssets="All" />
31 </ItemGroup>
32</Project>