@joebigelow / wix-1 / commits / 244b46cf

Migrate WixCop into Tools from wix4.

Sean Hall committed Sep 2, 2018 at 16:12 UTC 244b46cf7f3252d6dc3884ce184be901d1d173e5
22 files changed +1921 -69
Tools.sln
+18
@@ -18,6 +18,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
18 .editorconfig = .editorconfig
19 EndProjectSection
20 EndProject
21 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WixCop", "src\wixcop\WixCop.csproj", "{2E54120B-8958-40B1-A7FC-851446994CD8}"
22 +EndProject
23 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WixToolset.Tools.Core", "src\WixToolset.Tools.Core\WixToolset.Tools.Core.csproj", "{9C3B486F-AE0E-43BA-823A-30808B73C6B4}"
24 +EndProject
25 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WixCopTests", "src\test\wixcop\WixCopTests.csproj", "{F1A8112B-95A1-4AF7-81CB-523BE7DB8E5C}"
26 +EndProject
27 Global
28 GlobalSection(SolutionConfigurationPlatforms) = preSolution
29 Debug|Any CPU = Debug|Any CPU
@@ -44,6 +50,18 @@ Global
50 {0DF5D4CF-8457-469D-8288-13775E984F70}.Debug|Any CPU.Build.0 = Debug|Any CPU
51 {0DF5D4CF-8457-469D-8288-13775E984F70}.Release|Any CPU.ActiveCfg = Release|Any CPU
52 {0DF5D4CF-8457-469D-8288-13775E984F70}.Release|Any CPU.Build.0 = Release|Any CPU
53 + {2E54120B-8958-40B1-A7FC-851446994CD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
54 + {2E54120B-8958-40B1-A7FC-851446994CD8}.Debug|Any CPU.Build.0 = Debug|Any CPU
55 + {2E54120B-8958-40B1-A7FC-851446994CD8}.Release|Any CPU.ActiveCfg = Release|Any CPU
56 + {2E54120B-8958-40B1-A7FC-851446994CD8}.Release|Any CPU.Build.0 = Release|Any CPU
57 + {9C3B486F-AE0E-43BA-823A-30808B73C6B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
58 + {9C3B486F-AE0E-43BA-823A-30808B73C6B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
59 + {9C3B486F-AE0E-43BA-823A-30808B73C6B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
60 + {9C3B486F-AE0E-43BA-823A-30808B73C6B4}.Release|Any CPU.Build.0 = Release|Any CPU
61 + {F1A8112B-95A1-4AF7-81CB-523BE7DB8E5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
62 + {F1A8112B-95A1-4AF7-81CB-523BE7DB8E5C}.Debug|Any CPU.Build.0 = Debug|Any CPU
63 + {F1A8112B-95A1-4AF7-81CB-523BE7DB8E5C}.Release|Any CPU.ActiveCfg = Release|Any CPU
64 + {F1A8112B-95A1-4AF7-81CB-523BE7DB8E5C}.Release|Any CPU.Build.0 = Release|Any CPU
65 EndGlobalSection
66 GlobalSection(SolutionProperties) = preSolution
67 HideSolutionNode = FALSE
src/WixToolset.BuildTasks/ConvertReferences.cs
+2 -5
@@ -3,13 +3,10 @@
3 namespace WixToolset.BuildTasks
4 {
5 using System;
6 - using System.Collections;
6 using System.Collections.Generic;
8 - using System.Globalization;
9 - using System.IO;
10 - using System.Xml;
7 using Microsoft.Build.Framework;
8 using Microsoft.Build.Utilities;
9 + using WixToolset.Tools.Core;
10
11 /// <summary>
12 /// This task assigns Culture metadata to files based on the value of the Culture attribute on the
@@ -62,7 +59,7 @@ namespace WixToolset.BuildTasks
59 {
60 Dictionary<string, string> newItemMetadeta = new Dictionary<string, string>();
61
65 - if (!String.IsNullOrEmpty(item.GetMetadata(Common.DoNotHarvest)))
62 + if (!String.IsNullOrEmpty(item.GetMetadata(ToolsCommon.DoNotHarvest)))
63 {
64 continue;
65 }
src/WixToolset.BuildTasks/RefreshBundleGeneratedFile.cs
+3 -6
@@ -6,19 +6,16 @@ namespace WixToolset.BuildTasks
6 using System.Collections;
7 using System.Globalization;
8 using System.IO;
9 - using System.Text.RegularExpressions;
9 using System.Xml;
10 using Microsoft.Build.Framework;
11 using Microsoft.Build.Utilities;
12 + using WixToolset.Tools.Core;
13
14 /// <summary>
15 /// This task refreshes the generated file for bundle projects.
16 /// </summary>
17 public class RefreshBundleGeneratedFile : Task
18 {
19 - private static readonly Regex AddPrefix = new Regex(@"^[^a-zA-Z_]", RegexOptions.Compiled);
20 - private static readonly Regex IllegalIdentifierCharacters = new Regex(@"[^A-Za-z0-9_\.]|\.{2,}", RegexOptions.Compiled); // non 'words' and assorted valid characters
21 -
19 private ITaskItem[] generatedFiles;
20 private ITaskItem[] projectReferencePaths;
21
@@ -54,14 +51,14 @@ namespace WixToolset.BuildTasks
51 {
52 ITaskItem item = this.ProjectReferencePaths[i];
53
57 - if (!String.IsNullOrEmpty(item.GetMetadata(Common.DoNotHarvest)))
54 + if (!String.IsNullOrEmpty(item.GetMetadata(ToolsCommon.DoNotHarvest)))
55 {
56 continue;
57 }
58
59 string projectPath = CreateProjectReferenceDefineConstants.GetProjectPath(this.ProjectReferencePaths, i);
60 string projectName = Path.GetFileNameWithoutExtension(projectPath);
64 - string referenceName = Common.GetIdentifierFromName(CreateProjectReferenceDefineConstants.GetReferenceName(item, projectName));
61 + string referenceName = ToolsCommon.GetIdentifierFromName(CreateProjectReferenceDefineConstants.GetReferenceName(item, projectName));
62
63 string[] pogs = item.GetMetadata("RefProjectOutputGroups").Split(';');
64 foreach (string pog in pogs)
src/WixToolset.BuildTasks/RefreshGeneratedFile.cs
+3 -6
@@ -6,10 +6,10 @@ namespace WixToolset.BuildTasks
6 using System.Collections;
7 using System.Globalization;
8 using System.IO;
9 - using System.Text.RegularExpressions;
9 using System.Xml;
10 using Microsoft.Build.Framework;
11 using Microsoft.Build.Utilities;
12 + using WixToolset.Tools.Core;
13
14 /// <summary>
15 /// This task refreshes the generated file that contains ComponentGroupRefs
@@ -17,9 +17,6 @@ namespace WixToolset.BuildTasks
17 /// </summary>
18 public class RefreshGeneratedFile : Task
19 {
20 - private static readonly Regex AddPrefix = new Regex(@"^[^a-zA-Z_]", RegexOptions.Compiled);
21 - private static readonly Regex IllegalIdentifierCharacters = new Regex(@"[^A-Za-z0-9_\.]|\.{2,}", RegexOptions.Compiled); // non 'words' and assorted valid characters
22 -
20 private ITaskItem[] generatedFiles;
21 private ITaskItem[] projectReferencePaths;
22
@@ -54,14 +51,14 @@ namespace WixToolset.BuildTasks
51 {
52 ITaskItem item = this.ProjectReferencePaths[i];
53
57 - if (!String.IsNullOrEmpty(item.GetMetadata(Common.DoNotHarvest)))
54 + if (!String.IsNullOrEmpty(item.GetMetadata(ToolsCommon.DoNotHarvest)))
55 {
56 continue;
57 }
58
59 string projectPath = CreateProjectReferenceDefineConstants.GetProjectPath(this.ProjectReferencePaths, i);
60 string projectName = Path.GetFileNameWithoutExtension(projectPath);
64 - string referenceName = Common.GetIdentifierFromName(CreateProjectReferenceDefineConstants.GetReferenceName(item, projectName));
61 + string referenceName = ToolsCommon.GetIdentifierFromName(CreateProjectReferenceDefineConstants.GetReferenceName(item, projectName));
62
63 string[] pogs = item.GetMetadata("RefProjectOutputGroups").Split(';');
64 foreach (string pog in pogs)
src/WixToolset.BuildTasks/WixToolset.BuildTasks.csproj
+4
@@ -27,6 +27,10 @@
27 <Content Include="wix.signing.targets" CopyToOutputDirectory="PreserveNewest" />
28 </ItemGroup>
29
30 + <ItemGroup>
31 + <ProjectReference Include="..\WixToolset.Tools.Core\WixToolset.Tools.Core.csproj" />
32 + </ItemGroup>
33 +
34 <ItemGroup>
35 <ProjectReference Include="$(WixToolsetRootFolder)\Core\src\WixToolset.Core\WixToolset.Core.csproj" Condition=" '$(Configuration)' == 'Debug' And Exists('$(WixToolsetRootFolder)\Core\README.md') " />
36 <PackageReference Include="WixToolset.Core" Version="4.0.*" Condition=" '$(Configuration)' == 'Release' Or !Exists('$(WixToolsetRootFolder)\Core\README.md') " />
src/WixToolset.Tools.Core/ConsoleMessageListener.cs new
+56
@@ -0,0 +1,56 @@
1 +using System;
2 +using System.Globalization;
3 +using System.Text;
4 +using System.Threading;
5 +using WixToolset.Data;
6 +using WixToolset.Extensibility;
7 +
8 +namespace WixToolset.Tools.Core
9 +{
10 + public sealed class ConsoleMessageListener : IMessageListener
11 + {
12 + public ConsoleMessageListener(string shortName, string longName)
13 + {
14 + this.ShortAppName = shortName;
15 + this.LongAppName = longName;
16 +
17 + PrepareConsoleForLocalization();
18 + }
19 +
20 + public string LongAppName { get; }
21 +
22 + public string ShortAppName { get; }
23 +
24 + public void Write(Message message)
25 + {
26 + var filename = message.SourceLineNumbers?.FileName ?? this.LongAppName;
27 + var line = message.SourceLineNumbers?.LineNumber ?? -1;
28 + var type = message.Level.ToString().ToLowerInvariant();
29 + var output = message.Level >= MessageLevel.Warning ? Console.Out : Console.Error;
30 +
31 + if (line > 0)
32 + {
33 + filename = String.Concat(filename, "(", line, ")");
34 + }
35 +
36 + output.WriteLine("{0} : {1} {2}{3:0000}: {4}", filename, type, this.ShortAppName, message.Id, message.ToString());
37 + }
38 +
39 + public void Write(string message)
40 + {
41 + Console.Out.WriteLine(message);
42 + }
43 +
44 + private static void PrepareConsoleForLocalization()
45 + {
46 + Thread.CurrentThread.CurrentUICulture = CultureInfo.CurrentUICulture.GetConsoleFallbackUICulture();
47 +
48 + if (Console.OutputEncoding.CodePage != Encoding.UTF8.CodePage &&
49 + Console.OutputEncoding.CodePage != Thread.CurrentThread.CurrentUICulture.TextInfo.OEMCodePage &&
50 + Console.OutputEncoding.CodePage != Thread.CurrentThread.CurrentUICulture.TextInfo.ANSICodePage)
51 + {
52 + Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
53 + }
54 + }
55 + }
56 +}
src/WixToolset.Tools.Core/WixToolset.Tools.Core.csproj new
+28
@@ -0,0 +1,28 @@
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 + <TargetFramework>netstandard2.0</TargetFramework>
7 + <Description>Tools Core</Description>
8 + <Title>WiX Toolset Tools Core</Title>
9 + <DebugType>embedded</DebugType>
10 + <PublishRepositoryUrl>true</PublishRepositoryUrl>
11 + </PropertyGroup>
12 +
13 + <ItemGroup>
14 + <ProjectReference Include="$(WixToolsetRootFolder)\Data\src\WixToolset.Data\WixToolset.Data.csproj" Condition=" '$(Configuration)' == 'Debug' And Exists('$(WixToolsetRootFolder)\Data\README.md') " />
15 + <PackageReference Include="WixToolset.Data" Version="4.0.*" Condition=" '$(Configuration)' == 'Release' Or !Exists('$(WixToolsetRootFolder)\Data\README.md') " />
16 +
17 + <ProjectReference Include="$(WixToolsetRootFolder)\Extensibility\src\WixToolset.Extensibility\WixToolset.Extensibility.csproj" Condition=" '$(Configuration)' == 'Debug' And Exists('$(WixToolsetRootFolder)\Extensibility\README.md') " />
18 + <PackageReference Include="WixToolset.Extensibility" Version="4.0.*" Condition=" '$(Configuration)' == 'Release' Or !Exists('$(WixToolsetRootFolder)\Extensibility\README.md') " />
19 +
20 + <ProjectReference Include="$(WixToolsetRootFolder)\Core\src\WixToolset.Core\WixToolset.Core.csproj" Condition=" '$(Configuration)' == 'Debug' And Exists('$(WixToolsetRootFolder)\Core\README.md') " />
21 + <PackageReference Include="WixToolset.Core" Version="4.0.*" Condition=" '$(Configuration)' == 'Release' Or !Exists('$(WixToolsetRootFolder)\Core\README.md') " />
22 + </ItemGroup>
23 +
24 + <ItemGroup>
25 + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta-63102-01" PrivateAssets="All"/>
26 + <PackageReference Include="Nerdbank.GitVersioning" Version="2.1.65" PrivateAssets="All" />
27 + </ItemGroup>
28 +</Project>
src/Wixtoolset.Tools.Core/ToolsCommon.cs renamed
+3 -5
@@ -1,16 +1,14 @@
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
3 -namespace WixToolset
3 +namespace WixToolset.Tools.Core
4 {
5 using System;
6 - using System.Globalization;
7 - using System.Text;
6 using System.Text.RegularExpressions;
7
8 /// <summary>
9 /// Common WixTasks utility methods and types.
10 /// </summary>
13 - internal static class Common
11 + public static class ToolsCommon
12 {
13 /// <summary>Metadata key name to turn off harvesting of project references.</summary>
14 public const string DoNotHarvest = "DoNotHarvest";
@@ -24,7 +22,7 @@ namespace WixToolset
22 /// <param name="name">File/directory name to generate identifer from</param>
23 /// <returns>A version of the name that is a legal identifier.</returns>
24 /// <remarks>This is duplicated from WiX's Common class.</remarks>
27 - internal static string GetIdentifierFromName(string name)
25 + public static string GetIdentifierFromName(string name)
26 {
27 string result = IllegalIdentifierCharacters.Replace(name, "_"); // replace illegal characters with "_".
28
src/test/wixcop/ConverterFixture.cs new
+418
@@ -0,0 +1,418 @@
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 +
3 +namespace WixTest.WixUnitTest
4 +{
5 + using System;
6 + using System.IO;
7 + using System.Text;
8 + using System.Xml.Linq;
9 + using WixCop;
10 + using WixToolset;
11 + using WixToolset.Data;
12 + using WixToolset.Extensibility;
13 + using WixToolset.Extensibility.Services;
14 + using Xunit;
15 +
16 + public class ConverterFixture
17 + {
18 + private static readonly XNamespace Wix4Namespace = "http://wixtoolset.org/schemas/v4/wxs";
19 +
20 + [Fact]
21 + public void EnsuresDeclaration()
22 + {
23 + string parse = String.Join(Environment.NewLine,
24 + "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
25 + " <Fragment />",
26 + "</Wix>");
27 +
28 + string expected = String.Join(Environment.NewLine,
29 + "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
30 + "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
31 + " <Fragment />",
32 + "</Wix>");
33 +
34 + XDocument document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
35 +
36 + var messaging = new DummyMessaging();
37 + Converter converter = new Converter(messaging, 2, null, null);
38 +
39 + int errors = converter.ConvertDocument(document);
40 +
41 + string actual = UnformattedDocumentString(document);
42 +
43 + Assert.Equal(1, errors);
44 + Assert.Equal(expected, actual);
45 + }
46 +
47 + [Fact]
48 + public void EnsuresUtf8Declaration()
49 + {
50 + string parse = String.Join(Environment.NewLine,
51 + "<?xml version='1.0'?>",
52 + "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
53 + " <Fragment />",
54 + "</Wix>");
55 +
56 + XDocument document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
57 +
58 + var messaging = new DummyMessaging();
59 + Converter converter = new Converter(messaging, 4, null, null);
60 +
61 + int errors = converter.ConvertDocument(document);
62 +
63 + Assert.Equal(1, errors);
64 + Assert.Equal("1.0", document.Declaration.Version);
65 + Assert.Equal("utf-8", document.Declaration.Encoding);
66 + }
67 +
68 + [Fact]
69 + public void CanFixWhitespace()
70 + {
71 + string parse = String.Join(Environment.NewLine,
72 + "<?xml version='1.0' encoding='utf-8'?>",
73 + "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
74 + " <Fragment>",
75 + " <Property Id='Prop'",
76 + " Value='Val'>",
77 + " </Property>",
78 + " </Fragment>",
79 + "</Wix>");
80 +
81 + string expected = String.Join(Environment.NewLine,
82 + "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
83 + "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
84 + " <Fragment>",
85 + " <Property Id=\"Prop\" Value=\"Val\" />",
86 + " </Fragment>",
87 + "</Wix>");
88 +
89 + XDocument document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
90 +
91 + var messaging = new DummyMessaging();
92 + Converter converter = new Converter(messaging, 4, null, null);
93 +
94 + int errors = converter.ConvertDocument(document);
95 +
96 + string actual = UnformattedDocumentString(document);
97 +
98 + Assert.Equal(4, errors);
99 + Assert.Equal(expected, actual);
100 + }
101 +
102 + [Fact]
103 + public void CanFixCdataWhitespace()
104 + {
105 + string parse = String.Join(Environment.NewLine,
106 + "<?xml version='1.0' encoding='utf-8'?>",
107 + "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
108 + " <Fragment>",
109 + " <Property Id='Prop'>",
110 + " <![CDATA[1<2]]>",
111 + " </Property>",
112 + " </Fragment>",
113 + "</Wix>");
114 +
115 + string expected = String.Join(Environment.NewLine,
116 + "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
117 + "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
118 + " <Fragment>",
119 + " <Property Id=\"Prop\"><![CDATA[1<2]]></Property>",
120 + " </Fragment>",
121 + "</Wix>");
122 +
123 + XDocument document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
124 +
125 + var messaging = new DummyMessaging();
126 + Converter converter = new Converter(messaging, 2, null, null);
127 +
128 + int errors = converter.ConvertDocument(document);
129 +
130 + string actual = UnformattedDocumentString(document);
131 +
132 + Assert.Equal(2, errors);
133 + Assert.Equal(expected, actual);
134 + }
135 +
136 + [Fact]
137 + public void CanConvertMainNamespace()
138 + {
139 + string parse = String.Join(Environment.NewLine,
140 + "<?xml version='1.0' encoding='utf-8'?>",
141 + "<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>",
142 + " <Fragment />",
143 + "</Wix>");
144 +
145 + string expected = String.Join(Environment.NewLine,
146 + "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
147 + "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
148 + " <Fragment />",
149 + "</Wix>");
150 +
151 + XDocument document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
152 +
153 + var messaging = new DummyMessaging();
154 + Converter converter = new Converter(messaging, 2, null, null);
155 +
156 + int errors = converter.ConvertDocument(document);
157 +
158 + string actual = UnformattedDocumentString(document);
159 +
160 + Assert.Equal(1, errors);
161 + //Assert.Equal(Wix4Namespace, document.Root.GetDefaultNamespace());
162 + Assert.Equal(expected, actual);
163 + }
164 +
165 + [Fact]
166 + public void CanConvertNamedMainNamespace()
167 + {
168 + string parse = String.Join(Environment.NewLine,
169 + "<?xml version='1.0' encoding='utf-8'?>",
170 + "<w:Wix xmlns:w='http://schemas.microsoft.com/wix/2006/wi'>",
171 + " <w:Fragment />",
172 + "</w:Wix>");
173 +
174 + string expected = String.Join(Environment.NewLine,
175 + "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
176 + "<w:Wix xmlns:w=\"http://wixtoolset.org/schemas/v4/wxs\">",
177 + " <w:Fragment />",
178 + "</w:Wix>");
179 +
180 + XDocument document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
181 +
182 + var messaging = new DummyMessaging();
183 + Converter converter = new Converter(messaging, 2, null, null);
184 +
185 + int errors = converter.ConvertDocument(document);
186 +
187 + string actual = UnformattedDocumentString(document);
188 +
189 + Assert.Equal(1, errors);
190 + Assert.Equal(expected, actual);
191 + Assert.Equal(Wix4Namespace, document.Root.GetNamespaceOfPrefix("w"));
192 + }
193 +
194 + [Fact]
195 + public void CanConvertNonWixDefaultNamespace()
196 + {
197 + string parse = String.Join(Environment.NewLine,
198 + "<?xml version='1.0' encoding='utf-8'?>",
199 + "<w:Wix xmlns:w='http://schemas.microsoft.com/wix/2006/wi' xmlns='http://schemas.microsoft.com/wix/UtilExtension'>",
200 + " <w:Fragment>",
201 + " <Test />",
202 + " </w:Fragment>",
203 + "</w:Wix>");
204 +
205 + string expected = String.Join(Environment.NewLine,
206 + "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
207 + "<w:Wix xmlns:w=\"http://wixtoolset.org/schemas/v4/wxs\" xmlns=\"http://wixtoolset.org/schemas/v4/wxs/util\">",
208 + " <w:Fragment>",
209 + " <Test />",
210 + " </w:Fragment>",
211 + "</w:Wix>");
212 +
213 + XDocument document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
214 +
215 + var messaging = new DummyMessaging();
216 + Converter converter = new Converter(messaging, 2, null, null);
217 +
218 + int errors = converter.ConvertDocument(document);
219 +
220 + string actual = UnformattedDocumentString(document);
221 +
222 + Assert.Equal(2, errors);
223 + Assert.Equal(expected, actual);
224 + Assert.Equal(Wix4Namespace, document.Root.GetNamespaceOfPrefix("w"));
225 + Assert.Equal("http://wixtoolset.org/schemas/v4/wxs/util", document.Root.GetDefaultNamespace());
226 + }
227 +
228 + [Fact]
229 + public void CanConvertExtensionNamespace()
230 + {
231 + string parse = String.Join(Environment.NewLine,
232 + "<?xml version='1.0' encoding='utf-8'?>",
233 + "<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi' xmlns:util='http://schemas.microsoft.com/wix/UtilExtension'>",
234 + " <Fragment />",
235 + "</Wix>");
236 +
237 + string expected = String.Join(Environment.NewLine,
238 + "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
239 + "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\" xmlns:util=\"http://wixtoolset.org/schemas/v4/wxs/util\">",
240 + " <Fragment />",
241 + "</Wix>");
242 +
243 + XDocument document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
244 +
245 + var messaging = new DummyMessaging();
246 + Converter converter = new Converter(messaging, 2, null, null);
247 +
248 + int errors = converter.ConvertDocument(document);
249 +
250 + string actual = UnformattedDocumentString(document);
251 +
252 + Assert.Equal(2, errors);
253 + Assert.Equal(expected, actual);
254 + Assert.Equal(Wix4Namespace, document.Root.GetDefaultNamespace());
255 + }
256 +
257 + [Fact]
258 + public void CanConvertMissingNamespace()
259 + {
260 + string parse = String.Join(Environment.NewLine,
261 + "<?xml version='1.0' encoding='utf-8'?>",
262 + "<Wix>",
263 + " <Fragment />",
264 + "</Wix>");
265 +
266 + string expected = String.Join(Environment.NewLine,
267 + "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
268 + "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
269 + " <Fragment />",
270 + "</Wix>");
271 +
272 + XDocument document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
273 +
274 + var messaging = new DummyMessaging();
275 + Converter converter = new Converter(messaging, 2, null, null);
276 +
277 + int errors = converter.ConvertDocument(document);
278 +
279 + string actual = UnformattedDocumentString(document);
280 +
281 + Assert.Equal(1, errors);
282 + Assert.Equal(expected, actual);
283 + Assert.Equal(Wix4Namespace, document.Root.GetDefaultNamespace());
284 + }
285 +
286 + [Fact]
287 + public void CanConvertAnonymousFile()
288 + {
289 + string parse = String.Join(Environment.NewLine,
290 + "<?xml version='1.0' encoding='utf-8'?>",
291 + "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
292 + " <File Source='path\\to\\foo.txt' />",
293 + "</Wix>");
294 +
295 + string expected = String.Join(Environment.NewLine,
296 + "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
297 + "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
298 + " <File Id=\"foo.txt\" Source=\"path\\to\\foo.txt\" />",
299 + "</Wix>");
300 +
301 + XDocument document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
302 +
303 + var messaging = new DummyMessaging();
304 + Converter converter = new Converter(messaging, 2, null, null);
305 +
306 + int errors = converter.ConvertDocument(document);
307 +
308 + string actual = UnformattedDocumentString(document);
309 +
310 + Assert.Equal(1, errors);
311 + Assert.Equal(expected, actual);
312 + }
313 +
314 + [Fact]
315 + public void CanConvertSuppressSignatureValidationNo()
316 + {
317 + string parse = String.Join(Environment.NewLine,
318 + "<?xml version='1.0' encoding='utf-8'?>",
319 + "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
320 + " <MsiPackage SuppressSignatureValidation='no' />",
321 + "</Wix>");
322 +
323 + string expected = String.Join(Environment.NewLine,
324 + "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
325 + "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
326 + " <MsiPackage EnableSignatureValidation=\"yes\" />",
327 + "</Wix>");
328 +
329 + XDocument document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
330 +
331 + var messaging = new DummyMessaging();
332 + Converter converter = new Converter(messaging, 2, null, null);
333 +
334 + int errors = converter.ConvertDocument(document);
335 +
336 + string actual = UnformattedDocumentString(document);
337 +
338 + Assert.Equal(1, errors);
339 + Assert.Equal(expected, actual);
340 + }
341 +
342 + [Fact]
343 + public void CanConvertSuppressSignatureValidationYes()
344 + {
345 + string parse = String.Join(Environment.NewLine,
346 + "<?xml version='1.0' encoding='utf-8'?>",
347 + "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
348 + " <Payload SuppressSignatureValidation='yes' />",
349 + "</Wix>");
350 +
351 + string expected = String.Join(Environment.NewLine,
352 + "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
353 + "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
354 + " <Payload />",
355 + "</Wix>");
356 +
357 + XDocument document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
358 +
359 + var messaging = new DummyMessaging();
360 + Converter converter = new Converter(messaging, 2, null, null);
361 +
362 + int errors = converter.ConvertDocument(document);
363 +
364 + string actual = UnformattedDocumentString(document);
365 +
366 + Assert.Equal(1, errors);
367 + Assert.Equal(expected, actual);
368 + }
369 +
370 + private static string UnformattedDocumentString(XDocument document)
371 + {
372 + StringBuilder sb = new StringBuilder();
373 +
374 + using (StringWriter writer = new StringWriter(sb))
375 + {
376 + document.Save(writer, SaveOptions.DisableFormatting | SaveOptions.OmitDuplicateNamespaces);
377 + }
378 +
379 + return sb.ToString();
380 + }
381 +
382 + private class DummyMessaging : IMessaging
383 + {
384 + public bool EncounteredError { get; set; }
385 +
386 + public int LastErrorNumber { get; set; }
387 +
388 + public bool ShowVerboseMessages { get; set; }
389 + public bool SuppressAllWarnings { get; set; }
390 + public bool WarningsAsError { get; set; }
391 +
392 + public void ElevateWarningMessage(int warningNumber)
393 + {
394 + }
395 +
396 + public string FormatMessage(Message message)
397 + {
398 + return "";
399 + }
400 +
401 + public void SetListener(IMessageListener listener)
402 + {
403 + }
404 +
405 + public void SuppressWarningMessage(int warningNumber)
406 + {
407 + }
408 +
409 + public void Write(Message message)
410 + {
411 + }
412 +
413 + public void Write(string message, bool verbose = false)
414 + {
415 + }
416 + }
417 + }
418 +}
src/test/wixcop/TestData/SingleFile/ConvertedSingleFile.wxs new
+60
@@ -0,0 +1,60 @@
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 +
5 +
6 +<?include WixVer.wxi ?>
7 +
8 +<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:swid="http://wixtoolset.org/schemas/v4/wxs/tag" xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
9 + <Product Id="*" Name="!(loc.ShortProduct) v$(var.WixMajorMinor) Core" Language="1033" Manufacturer="!(loc.Company)" Version="$(var.WixMsiProductVersion)" UpgradeCode="3618724B-2523-44F9-A908-866AA619504D">
10 + <Package Compressed="yes" InstallerVersion="200" SummaryCodepage="1252" InstallScope="perMachine" />
11 + <swid:Tag Regid="!(loc.Regid)" InstallDirectory="INSTALLFOLDER" />
12 +
13 + <MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed." />
14 +
15 + <MediaTemplate CabinetTemplate="core{0}.cab" />
16 +
17 + <Feature Id="Feature_WiX" Title="WiX Toolset" Level="1">
18 + <Component Id="Licensing" Directory="INSTALLFOLDER">
19 + <File Id="LICENSE.TXT" Source="LICENSE.TXT" />
20 + </Component>
21 +
22 + <Component Id="ProductRegistration" Directory="INSTALLFOLDER">
23 + <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
24 + <RegistryValue Name="InstallFolder" Value="[INSTALLFOLDER]" Type="string" />
25 + </RegistryKey>
26 + </Component>
27 +
28 + <Component Id="ProductFamilyRegistration" Directory="INSTALLFOLDER">
29 + <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajor).x">
30 + <RegistryValue Name="v$(var.WixMajorMinor)" Value="[INSTALLFOLDER]" Type="string" />
31 + </RegistryKey>
32 + </Component>
33 +
34 + <Component Id="ProductInformation" Directory="BinFolder">
35 + <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
36 + <RegistryValue Name="InstallRoot" Value="[BinFolder]" Type="string" />
37 + <RegistryValue Name="ProductVersion" Value="[ProductVersion]" Type="string" />
38 + </RegistryKey>
39 +
40 + <RemoveFolder Id="CleanupShortcutFolder" Directory="ShortcutFolder" On="uninstall" />
41 + </Component>
42 +
43 + <Component Directory="BinFolder">
44 + <File Id="wixtoolset.org.ico" Source="common\wixtoolset.org.ico" />
45 + <util:InternetShortcut Id="wixtoolset.org" Directory="ShortcutFolder" Name="WiX Home Page" Target="http://wixtoolset.org/" IconFile="file://[#wixtoolset.org.ico]" />
46 + </Component>
47 +
48 + <ComponentGroupRef Id="ToolsetComponents" />
49 + <ComponentGroupRef Id="ExtensionComponents" />
50 + <ComponentGroupRef Id="LuxComponents" />
51 + <ComponentGroupRef Id="DocComponents" />
52 + </Feature>
53 +
54 + <FeatureRef Id="Feature_MSBuild" />
55 + <FeatureRef Id="Feature_Intellisense2010" />
56 + <FeatureRef Id="Feature_Intellisense2012" />
57 + <FeatureRef Id="Feature_Intellisense2013" />
58 + <FeatureRef Id="Feature_Intellisense2015" />
59 + </Product>
60 +</Wix>
src/test/wixcop/TestData/SingleFile/SingleFile.wxs new
+61
@@ -0,0 +1,61 @@
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 +
5 +
6 +<?include WixVer.wxi ?>
7 +
8 +<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:swid="http://schemas.microsoft.com/wix/TagExtension" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
9 + <Product Id="*" Name="!(loc.ShortProduct) v$(var.WixMajorMinor) Core" Language="1033" Manufacturer="!(loc.Company)"
10 + Version="$(var.WixMsiProductVersion)" UpgradeCode="3618724B-2523-44F9-A908-866AA619504D">
11 + <Package Compressed="yes" InstallerVersion="200" SummaryCodepage="1252" InstallScope="perMachine" />
12 + <swid:Tag Regid="!(loc.Regid)" InstallDirectory="INSTALLFOLDER" />
13 +
14 + <MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed." />
15 +
16 + <MediaTemplate CabinetTemplate="core{0}.cab" />
17 +
18 + <Feature Id="Feature_WiX" Title="WiX Toolset" Level="1">
19 + <Component Id="Licensing" Directory="INSTALLFOLDER">
20 + <File Source="LICENSE.TXT" />
21 + </Component>
22 +
23 + <Component Id="ProductRegistration" Directory="INSTALLFOLDER">
24 + <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
25 + <RegistryValue Name="InstallFolder" Value="[INSTALLFOLDER]" Type="string" />
26 + </RegistryKey>
27 + </Component>
28 +
29 + <Component Id="ProductFamilyRegistration" Directory="INSTALLFOLDER">
30 + <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajor).x">
31 + <RegistryValue Name="v$(var.WixMajorMinor)" Value="[INSTALLFOLDER]" Type="string" />
32 + </RegistryKey>
33 + </Component>
34 +
35 + <Component Id="ProductInformation" Directory="BinFolder">
36 + <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
37 + <RegistryValue Name="InstallRoot" Value="[BinFolder]" Type="string"/>
38 + <RegistryValue Name="ProductVersion" Value="[ProductVersion]" Type="string" />
39 + </RegistryKey>
40 +
41 + <RemoveFolder Id="CleanupShortcutFolder" Directory="ShortcutFolder" On="uninstall" />
42 + </Component>
43 +
44 + <Component Directory="BinFolder">
45 + <File Source="common\wixtoolset.org.ico" />
46 + <util:InternetShortcut Id="wixtoolset.org" Directory="ShortcutFolder" Name="WiX Home Page" Target="http://wixtoolset.org/" IconFile="file://[#wixtoolset.org.ico]" />
47 + </Component>
48 +
49 + <ComponentGroupRef Id="ToolsetComponents" />
50 + <ComponentGroupRef Id="ExtensionComponents" />
51 + <ComponentGroupRef Id="LuxComponents" />
52 + <ComponentGroupRef Id="DocComponents" />
53 + </Feature>
54 +
55 + <FeatureRef Id="Feature_MSBuild" />
56 + <FeatureRef Id="Feature_Intellisense2010" />
57 + <FeatureRef Id="Feature_Intellisense2012" />
58 + <FeatureRef Id="Feature_Intellisense2013" />
59 + <FeatureRef Id="Feature_Intellisense2015" />
60 + </Product>
61 +</Wix>
src/test/wixcop/WixCopFixture.cs new
+107
@@ -0,0 +1,107 @@
1 +using System;
2 +using System.Collections.Generic;
3 +using System.IO;
4 +using System.Linq;
5 +using WixBuildTools.TestSupport;
6 +using WixCop.CommandLine;
7 +using WixCop.Interfaces;
8 +using WixToolset.Core;
9 +using WixToolset.Core.TestPackage;
10 +using WixToolset.Extensibility;
11 +using WixToolset.Extensibility.Services;
12 +using Xunit;
13 +
14 +namespace WixCopTests
15 +{
16 + public class WixCopFixture
17 + {
18 + [Fact]
19 + public void CanConvertSingleFile()
20 + {
21 + const string beforeFileName = "SingleFile.wxs";
22 + const string afterFileName = "ConvertedSingleFile.wxs";
23 + var folder = TestData.Get(@"TestData\SingleFile");
24 +
25 + using (var fs = new DisposableFileSystem())
26 + {
27 + var baseFolder = fs.GetFolder(true);
28 + var targetFile = Path.Combine(baseFolder, beforeFileName);
29 + File.Copy(Path.Combine(folder, beforeFileName), Path.Combine(baseFolder, beforeFileName));
30 +
31 + var runner = new WixCopRunner
32 + {
33 + FixErrors = true,
34 + SearchPatterns =
35 + {
36 + targetFile,
37 + },
38 + };
39 +
40 + var result = runner.Execute(out var messages);
41 +
42 + Assert.Equal(2, result);
43 +
44 + var actualLines = File.ReadAllLines(targetFile);
45 + var expectedLines = File.ReadAllLines(Path.Combine(folder, afterFileName));
46 + Assert.Equal(expectedLines, actualLines);
47 +
48 + var runner2 = new WixCopRunner
49 + {
50 + FixErrors = true,
51 + SearchPatterns =
52 + {
53 + targetFile,
54 + },
55 + };
56 +
57 + var result2 = runner2.Execute(out var messages2);
58 +
59 + Assert.Equal(0, result2);
60 + }
61 + }
62 +
63 + private class WixCopRunner
64 + {
65 + public bool FixErrors { get; set; }
66 +
67 + public List<string> SearchPatterns { get; } = new List<string>();
68 +
69 + public int Execute(out List<string> messages)
70 + {
71 + var argList = new List<string>();
72 + if (this.FixErrors)
73 + {
74 + argList.Add("-f");
75 + }
76 +
77 + foreach (string searchPattern in this.SearchPatterns)
78 + {
79 + argList.Add(searchPattern);
80 + }
81 +
82 + return WixCopRunner.Execute(argList.ToArray(), out messages);
83 + }
84 +
85 + public static int Execute(string[] args, out List<string> messages)
86 + {
87 + var listener = new TestMessageListener();
88 +
89 + var serviceProvider = new WixToolsetServiceProvider();
90 + serviceProvider.AddService<IMessageListener>((x, y) => listener);
91 + serviceProvider.AddService<IWixCopCommandLineParser>((x, y) => new WixCopCommandLineParser(x));
92 +
93 + var result = Execute(serviceProvider, args);
94 +
95 + var messaging = serviceProvider.GetService<IMessaging>();
96 + messages = listener.Messages.Select(x => messaging.FormatMessage(x)).ToList();
97 + return result;
98 + }
99 +
100 + public static int Execute(IServiceProvider serviceProvider, string[] args)
101 + {
102 + var wixcop = new WixCop.Program();
103 + return wixcop.Run(serviceProvider, args);
104 + }
105 + }
106 + }
107 +}
src/test/wixcop/WixCopTests.csproj new
+41
@@ -0,0 +1,41 @@
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 + <TargetFramework>net461</TargetFramework>
7 + <IsPackable>false</IsPackable>
8 + <DebugType>embedded</DebugType>
9 + </PropertyGroup>
10 + <ItemGroup>
11 + <None Remove="TestData\SingleFile\ConvertedSingleFile.wxs" />
12 + <None Remove="TestData\SingleFile\SingleFile.wxs" />
13 + </ItemGroup>
14 + <ItemGroup>
15 + <Content Include="TestData\SingleFile\ConvertedSingleFile.wxs">
16 + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
17 + </Content>
18 + <Content Include="TestData\SingleFile\SingleFile.wxs">
19 + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
20 + </Content>
21 + </ItemGroup>
22 +
23 + <ItemGroup>
24 + <ProjectReference Include="..\..\wixcop\WixCop.csproj" />
25 + </ItemGroup>
26 +
27 + <ItemGroup>
28 + <ProjectReference Include="$(WixToolsetRootFolder)\Core\src\WixToolset.Core.TestPackage\WixToolset.Core.TestPackage.csproj" Condition=" '$(Configuration)' == 'Debug' And Exists('$(WixToolsetRootFolder)\Core\README.md') " />
29 + <PackageReference Include="WixToolset.Core.TestPackage" Version="4.0.*" Condition=" '$(Configuration)' == 'Release' Or !Exists('$(WixToolsetRootFolder)\Core\README.md') " />
30 + </ItemGroup>
31 +
32 + <ItemGroup>
33 + <PackageReference Include="WixBuildTools.TestSupport" Version="4.0.*" />
34 + </ItemGroup>
35 +
36 + <ItemGroup>
37 + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
38 + <PackageReference Include="xunit" Version="2.4.0" />
39 + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
40 + </ItemGroup>
41 +</Project>
src/wix/Program.cs
+1 -47
@@ -13,6 +13,7 @@ namespace WixToolset.Tools
13 using WixToolset.Extensibility;
14 using WixToolset.Extensibility.Data;
15 using WixToolset.Extensibility.Services;
16 + using WixToolset.Tools.Core;
17
18 /// <summary>
19 /// Wix Toolset Command-Line Interface.
@@ -79,52 +80,5 @@ namespace WixToolset.Tools
80
81 return extensionManager;
82 }
82 -
83 - private class ConsoleMessageListener : IMessageListener
84 - {
85 - public ConsoleMessageListener(string shortName, string longName)
86 - {
87 - this.ShortAppName = shortName;
88 - this.LongAppName = longName;
89 -
90 - PrepareConsoleForLocalization();
91 - }
92 -
93 - public string LongAppName { get; }
94 -
95 - public string ShortAppName { get; }
96 -
97 - public void Write(Message message)
98 - {
99 - var filename = message.SourceLineNumbers?.FileName ?? this.LongAppName;
100 - var line = message.SourceLineNumbers?.LineNumber ?? -1;
101 - var type = message.Level.ToString().ToLowerInvariant();
102 - var output = message.Level >= MessageLevel.Warning ? Console.Out : Console.Error;
103 -
104 - if (line > 0)
105 - {
106 - filename = String.Concat(filename, "(", line, ")");
107 - }
108 -
109 - output.WriteLine("{0} : {1} {2}{3:0000}: {4}", filename, type, this.ShortAppName, message.Id, message.ToString());
110 - }
111 -
112 - public void Write(string message)
113 - {
114 - Console.Out.WriteLine(message);
115 - }
116 -
117 - private static void PrepareConsoleForLocalization()
118 - {
119 - Thread.CurrentThread.CurrentUICulture = CultureInfo.CurrentUICulture.GetConsoleFallbackUICulture();
120 -
121 - if (Console.OutputEncoding.CodePage != Encoding.UTF8.CodePage &&
122 - Console.OutputEncoding.CodePage != Thread.CurrentThread.CurrentUICulture.TextInfo.OEMCodePage &&
123 - Console.OutputEncoding.CodePage != Thread.CurrentThread.CurrentUICulture.TextInfo.ANSICodePage)
124 - {
125 - Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
126 - }
127 - }
128 - }
83 }
84 }
src/wix/wix.csproj
+4
@@ -16,6 +16,10 @@
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') " />
src/wixcop/CommandLine/ConvertCommand.cs new
+212
@@ -0,0 +1,212 @@
1 +using System;
2 +using System.Collections.Generic;
3 +using System.IO;
4 +using System.Linq;
5 +using System.Xml;
6 +using WixToolset.Extensibility.Data;
7 +using WixToolset.Extensibility.Services;
8 +
9 +namespace 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 +}
src/wixcop/CommandLine/HelpCommand.cs new
+25
@@ -0,0 +1,25 @@
1 +using System;
2 +using WixToolset.Extensibility.Data;
3 +
4 +namespace 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 +}
src/wixcop/CommandLine/WixCopCommandLineParser.cs new
+132
@@ -0,0 +1,132 @@
1 +using System;
2 +using System.Collections.Generic;
3 +using WixCop.Interfaces;
4 +using WixToolset.Core;
5 +using WixToolset.Extensibility.Data;
6 +using WixToolset.Extensibility.Services;
7 +
8 +namespace 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 +}
src/wixcop/Converter.cs new
+633
@@ -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 +
3 +namespace 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 +}
src/wixcop/Interfaces/IWixCopCommandLineParser.cs new
+11
@@ -0,0 +1,11 @@
1 +using WixToolset.Extensibility.Data;
2 +
3 +namespace WixCop.Interfaces
4 +{
5 + public interface IWixCopCommandLineParser
6 + {
7 + ICommandLineArguments Arguments { get; set; }
8 +
9 + ICommandLineCommand ParseWixCopCommandLine();
10 + }
11 +}
src/wixcop/Program.cs new
+67
@@ -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 +
3 +namespace 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 +}
src/wixcop/WixCop.csproj new
+32
@@ -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>