├── ProjectSimplifier
├── InternalsVisibleTo.cs
├── GlobalSuppressions.cs
├── ProjectStyle.cs
├── Properties
│ ├── PublishProfiles
│ │ └── FolderProfile.pubxml
│ └── launchSettings.json
├── ProjectSimplifier.csproj
├── BaselineProject.cs
├── ProjectItemComparer.cs
├── ItemsDiff.cs
├── UnconfiguredProject.cs
├── PropertiesDiff.cs
├── Options.cs
├── MSBuildProjectRootElement.cs
├── Facts.cs
├── ProjectExtensions.cs
├── MSBuildProject.cs
├── Program.cs
├── Differ.cs
├── MSBuildUtilities.cs
├── ProjectLoader.cs
└── Converter.cs
├── ProjectSimplifier.Tests
├── GlobalSuppressions.cs
├── ProjectSimplifier.Tests.csproj
├── ProjectExtensionsTests.cs
├── PropertiesDiffTests.cs
├── Mocks
│ └── IProjectFactory.cs
└── ItemsDiffTests.cs
├── .travis.yml
├── LICENSE
├── ProjectSimplifier.sln
├── README.md
├── .gitattributes
└── .gitignore
/ProjectSimplifier/InternalsVisibleTo.cs:
--------------------------------------------------------------------------------
1 | [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ProjectSimplifier.Tests")]
2 |
--------------------------------------------------------------------------------
/ProjectSimplifier/GlobalSuppressions.cs:
--------------------------------------------------------------------------------
1 |
2 | // This file is used by Code Analysis to maintain SuppressMessage
3 | // attributes that are applied to this project.
4 | // Project-level suppressions either have no target or are given
5 | // a specific target and scoped to a namespace, type, member, etc.
6 |
7 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0033:Use explicitly provided tuple name", Justification = "Appveyor doesnt support 15.3 yet.")]
8 |
9 |
--------------------------------------------------------------------------------
/ProjectSimplifier.Tests/GlobalSuppressions.cs:
--------------------------------------------------------------------------------
1 |
2 | // This file is used by Code Analysis to maintain SuppressMessage
3 | // attributes that are applied to this project.
4 | // Project-level suppressions either have no target or are given
5 | // a specific target and scoped to a namespace, type, member, etc.
6 |
7 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0033:Use explicitly provided tuple name", Justification = "Appveyor doesnt support 15.3 yet.")]
8 |
9 |
--------------------------------------------------------------------------------
/ProjectSimplifier/ProjectStyle.cs:
--------------------------------------------------------------------------------
1 | namespace ProjectSimplifier
2 | {
3 | internal enum ProjectStyle
4 | {
5 | ///
6 | /// The project has an import of Common.props and CSharp.targets.
7 | ///
8 | Default,
9 |
10 | ///
11 | /// The project imports props and targets but not the default ones.
12 | ///
13 | DefaultWithCustomTargets,
14 |
15 | ///
16 | /// Has more imports and the shape is unknown.
17 | ///
18 | Custom
19 | }
20 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: csharp
2 |
3 | matrix:
4 | include:
5 | - os: linux
6 | dist: trusty
7 | sudo: required
8 | - os: osx
9 |
10 | solution: solution-name.sln
11 | mono: beta
12 |
13 | before_install:
14 | - wget https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -O /tmp/nuget.exe
15 | - mono /tmp/nuget.exe install xunit.runner.console -Version 2.2.0 -Output /tmp/packages/
16 | install:
17 | - msbuild /t:restore
18 |
19 | script:
20 | - msbuild
21 | - mono /tmp/packages/xunit.runner.console.2.2.0/tools/xunit.console.exe ./ProjectSimplifier.Tests/bin/Debug/net461/ProjectSimplifier.Tests.dll
22 |
--------------------------------------------------------------------------------
/ProjectSimplifier/Properties/PublishProfiles/FolderProfile.pubxml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | FileSystem
9 | Release
10 | netcoreapp1.0
11 | bin\Release\PublishOutput
12 |
13 |
--------------------------------------------------------------------------------
/ProjectSimplifier/ProjectSimplifier.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/ProjectSimplifier/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "Diff": {
4 | "commandName": "Project",
5 | "commandLineArgs": "diff \"C:\\Users\\srivatsn\\Documents\\Visual Studio 2017\\Projects\\ConsoleApp24\\ConsoleApp24\\ConsoleApp24.csproj\" -r \"C:\\Program Files\\dotnet\\sdk\\1.0.0\\Roslyn\"",
6 | "environmentVariables": {
7 | "RoslynTargetsPath": "C:\\Program Files\\dotnet\\sdk\\1.0.0\\Roslyn",
8 | "MSBuildSdksPath": "C:\\Program Files\\dotnet\\sdk\\1.0.0\\Sdks\\"
9 | }
10 | },
11 | "Convert": {
12 | "commandName": "Project",
13 | "commandLineArgs": "convert C:\\roslyn\\src\\Compilers\\Core\\Portable\\CodeAnalysis.csproj -m \"C:\\Program Files (x86)\\Microsoft Visual Studio\\Preview\\Enterprise\\MSBuild\\15.0\\Bin\" -p EnableDefaultItems=true"
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/ProjectSimplifier/BaselineProject.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Immutable;
3 |
4 | namespace ProjectSimplifier
5 | {
6 | internal struct BaselineProject
7 | {
8 | public readonly ImmutableArray GlobalProperties;
9 | public readonly ImmutableDictionary TargetProjectProperties;
10 | public readonly UnconfiguredProject Project;
11 | public readonly ProjectStyle ProjectStyle;
12 |
13 | public BaselineProject(UnconfiguredProject project, ImmutableArray globalProperties, ImmutableDictionary targetProjectProperties, ProjectStyle projectStyle) : this()
14 | {
15 | GlobalProperties = globalProperties;
16 | TargetProjectProperties = targetProjectProperties;
17 | Project = project ?? throw new ArgumentNullException(nameof(project));
18 | ProjectStyle = projectStyle;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ProjectSimplifier.Tests/ProjectSimplifier.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | false
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Srivatsn Narayanan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/ProjectSimplifier.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26606.0
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectSimplifier", "ProjectSimplifier\ProjectSimplifier.csproj", "{AAC442C6-A00F-4EA5-A992-2F1E9CEEB6F7}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProjectSimplifier.Tests", "ProjectSimplifier.Tests\ProjectSimplifier.Tests.csproj", "{1E4BD1E1-C0F6-4216-AB17-F7EA8C461FC1}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {AAC442C6-A00F-4EA5-A992-2F1E9CEEB6F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {AAC442C6-A00F-4EA5-A992-2F1E9CEEB6F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {AAC442C6-A00F-4EA5-A992-2F1E9CEEB6F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {AAC442C6-A00F-4EA5-A992-2F1E9CEEB6F7}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {1E4BD1E1-C0F6-4216-AB17-F7EA8C461FC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {1E4BD1E1-C0F6-4216-AB17-F7EA8C461FC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {1E4BD1E1-C0F6-4216-AB17-F7EA8C461FC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {1E4BD1E1-C0F6-4216-AB17-F7EA8C461FC1}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | EndGlobal
29 |
--------------------------------------------------------------------------------
/ProjectSimplifier/ProjectItemComparer.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 |
4 | namespace ProjectSimplifier
5 | {
6 | public class ProjectItemComparer : IEqualityComparer
7 | {
8 | private readonly bool _compareMetadata;
9 |
10 | public static ProjectItemComparer IncludeComparer = new ProjectItemComparer(compareMetadata: false);
11 | public static ProjectItemComparer MetadataComparer = new ProjectItemComparer(compareMetadata: true);
12 |
13 | private ProjectItemComparer(bool compareMetadata)
14 | {
15 | _compareMetadata = compareMetadata;
16 | }
17 |
18 | public bool Equals(IProjectItem x, IProjectItem y)
19 | {
20 | // If y has all the metadata that x has then we declare them as equal. This is because
21 | // the sdk can add new metadata but there's not reason to remove them during conversion.
22 | var metadataEqual = _compareMetadata ?
23 | x.DirectMetadata.All(xmd => y.DirectMetadata.Any(
24 | ymd => xmd.Name.Equals(ymd.Name, System.StringComparison.OrdinalIgnoreCase) &&
25 | xmd.EvaluatedValue.Equals(ymd.EvaluatedValue, System.StringComparison.OrdinalIgnoreCase)))
26 | : true;
27 |
28 | return x.ItemType == y.ItemType && x.EvaluatedInclude.Equals(y.EvaluatedInclude, System.StringComparison.OrdinalIgnoreCase) && metadataEqual;
29 | }
30 |
31 | public int GetHashCode(IProjectItem obj)
32 | {
33 | return (obj.EvaluatedInclude.ToLowerInvariant() + obj.ItemType).GetHashCode();
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ProjectSimplifier
2 | This is a tool that can be used to help with the conversion of old-style csprojs to ones based on the .NET SDK.
3 |
4 | [](https://ci.appveyor.com/project/SrivatsnNarayanan/msbuildsdkdiffer)
5 |
6 | # What does the tool do?
7 | It loads up a given project and evaluates it to get a list of all properties and items. It then replaces the project in memory with a simple .NET SDK based template and then re-evaluates it.
8 | It does the second evaluation in the same project folder so that items that are automatically picked up by globbing will be known as well. It then produces a diff of the two states to identify the following:
9 | - Properties that can now be removed from the project because they are already implicitly defined by the SDK and the project had the default value.
10 | - Properties that need to be kept in the project either because they override the default or it's a property not defined in the SDK.
11 | - Items that can be removed because they are implicitly brought in by globs in the SDK
12 | - Items that need to be changed to the Update syntax because although they're brought by the SDK, there is extra metadata being added.
13 | - Items that need to be kept because theyr are not implicit in the SDK.
14 |
15 | # Usage:
16 |
17 | From a VS 2017 Developer command prompt
18 |
19 | ProjectSimplifier convert a.csproj -out:b.csproj
20 |
21 | From a regular command prompt
22 |
23 | ProjectSimplifier convert a.csproj -out:b.csproj -m:``
24 |
25 |
26 | Caveats: If your project has custom imports, you might be changing semantics in a very subtle way by moving to the SDK and this tool doesnt know to find those cases.
27 |
28 |
--------------------------------------------------------------------------------
/ProjectSimplifier/ItemsDiff.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Collections.Immutable;
3 | using System.Linq;
4 |
5 | namespace ProjectSimplifier
6 | {
7 | internal struct ItemsDiff
8 | {
9 | public readonly string ItemType;
10 | public readonly ImmutableArray DefaultedItems;
11 | public readonly ImmutableArray NotDefaultedItems;
12 | public readonly ImmutableArray IntroducedItems;
13 | public readonly ImmutableArray ChangedItems;
14 |
15 | public ItemsDiff(string itemType, ImmutableArray defaultedItems, ImmutableArray notDefaultedItems, ImmutableArray introducedItems, ImmutableArray changedItems) : this()
16 | {
17 | ItemType = itemType;
18 | DefaultedItems = defaultedItems;
19 | NotDefaultedItems = notDefaultedItems;
20 | IntroducedItems = introducedItems;
21 | ChangedItems = changedItems;
22 | }
23 |
24 | public ImmutableArray GetDiffLines()
25 | {
26 | var lines = ImmutableArray.CreateBuilder();
27 |
28 | if (!DefaultedItems.IsEmpty || !NotDefaultedItems.IsEmpty || !IntroducedItems.IsEmpty || !ChangedItems.IsEmpty)
29 | {
30 | lines.Add($"{ ItemType} items:");
31 | if (!DefaultedItems.IsEmpty)
32 | {
33 | lines.AddRange(DefaultedItems.Select(s => $"- {s.EvaluatedInclude}"));
34 | }
35 |
36 | if (!NotDefaultedItems.IsEmpty)
37 | {
38 | lines.AddRange(NotDefaultedItems.Select(s => $"= {s.EvaluatedInclude}"));
39 | }
40 |
41 | if (!IntroducedItems.IsEmpty)
42 | {
43 | lines.AddRange(IntroducedItems.Select(s => $"+ {s.EvaluatedInclude}"));
44 | }
45 |
46 | lines.Add("");
47 | }
48 |
49 | return lines.ToImmutable();
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/ProjectSimplifier/UnconfiguredProject.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 | using System.Linq;
3 | using Microsoft.Build.Construction;
4 | using Microsoft.Build.Evaluation;
5 |
6 | namespace ProjectSimplifier
7 | {
8 | class UnconfiguredProject
9 | {
10 | public ImmutableDictionary ConfiguredProjects { get; private set; }
11 |
12 | public IProject FirstConfiguredProject => ConfiguredProjects.First().Value;
13 |
14 | public ImmutableDictionary> Configurations { get; }
15 |
16 | public UnconfiguredProject(ImmutableDictionary> configurations)
17 | {
18 | Configurations = configurations;
19 | }
20 |
21 | internal void LoadProjects(ProjectCollection collection, ImmutableDictionary globalProperties, string projectFilePath)
22 | {
23 | var projectBuilder = ImmutableDictionary.CreateBuilder();
24 | foreach (var config in Configurations)
25 | {
26 | var globalPropertiesWithDimensions = globalProperties.AddRange(config.Value);
27 | var project = new MSBuildProject(collection.LoadProject(projectFilePath, globalPropertiesWithDimensions, toolsVersion: null));
28 | projectBuilder.Add(config.Key, project);
29 |
30 | }
31 |
32 | ConfiguredProjects = projectBuilder.ToImmutable();
33 | }
34 |
35 | internal void LoadProjects(ProjectCollection collection, ImmutableDictionary globalProperties, ProjectRootElement rootElement)
36 | {
37 | var projectBuilder = ImmutableDictionary.CreateBuilder();
38 | foreach (var config in Configurations)
39 | {
40 | var project = new MSBuildProject(new Project(rootElement, config.Value, null, collection));
41 | projectBuilder.Add(config.Key, project);
42 | }
43 |
44 | ConfiguredProjects = projectBuilder.ToImmutable();
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/ProjectSimplifier/PropertiesDiff.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 | using System.Linq;
3 | using Microsoft.Build.Evaluation;
4 |
5 | namespace ProjectSimplifier
6 | {
7 | internal struct PropertiesDiff
8 | {
9 | public readonly ImmutableArray DefaultedProperties;
10 | public readonly ImmutableArray NotDefaultedProperties;
11 | public readonly ImmutableArray<(IProjectProperty oldProp, IProjectProperty newProp)> ChangedProperties;
12 |
13 | public PropertiesDiff(ImmutableArray defaultedProperties, ImmutableArray notDefaultedPropeties, ImmutableArray<(IProjectProperty, IProjectProperty)> changedProperties) : this()
14 | {
15 | DefaultedProperties = defaultedProperties;
16 | NotDefaultedProperties = notDefaultedPropeties;
17 | ChangedProperties = changedProperties;
18 | }
19 |
20 | public ImmutableArray GetDiffLines()
21 | {
22 | var lines = ImmutableArray.CreateBuilder();
23 |
24 | if (!DefaultedProperties.IsEmpty)
25 | {
26 | lines.Add("Properties that are defaulted by the SDK:");
27 | lines.AddRange(DefaultedProperties.Select(prop => $"- {prop.Name} = {prop.EvaluatedValue}"));
28 | lines.Add("");
29 | }
30 | if (!NotDefaultedProperties.IsEmpty)
31 | {
32 | lines.Add("Properties that are not defaulted by the SDK:");
33 | lines.AddRange(NotDefaultedProperties.Select(prop => $"+ {prop.Name} = {prop.EvaluatedValue}"));
34 | lines.Add("");
35 | }
36 | if (!ChangedProperties.IsEmpty)
37 | {
38 | lines.Add("Properties whose value is different from the SDK's default:");
39 | var changedProps = ChangedProperties.SelectMany((diff) =>
40 | new[]
41 | {
42 | $"- {diff.oldProp.Name} = {diff.oldProp.EvaluatedValue}",
43 | $"+ {diff.newProp.Name} = {diff.newProp.EvaluatedValue}"
44 | }
45 | );
46 | lines.AddRange(changedProps);
47 | lines.Add("");
48 | }
49 |
50 | return lines.ToImmutable();
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/ProjectSimplifier/Options.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using CommandLine;
3 |
4 | namespace ProjectSimplifier
5 | {
6 | internal class Options
7 | {
8 | [Value(0, HelpText = "Full path to the project file", Required = true)]
9 | public string ProjectFilePath { get; set; }
10 |
11 | [Option('r', "roslyntargetspath",
12 | HelpText = "Path to the roslyn targets")]
13 | public string RoslynTargetsPath { get; set; }
14 |
15 | [Option('s', "msbuildsdkdspath",
16 | HelpText = "Path to the MSBuild SDKs")]
17 | public string MSBuildSdksPath { get; set; }
18 |
19 | [Option('m', "msbuildpath",
20 | HelpText = "Path to the MSBuild.exe")]
21 | public string MSBuildPath { get; set; }
22 |
23 | [Option('p', "properties",
24 | HelpText = "Properties to set in the target project before converting")]
25 | public IEnumerable TargetProjectProperties { get; set; }
26 | }
27 |
28 | [Verb("log", HelpText = "Log properties and items in the project and in a SDK-based baseline")]
29 | internal class LogOptions : Options
30 | {
31 | [Option('c', "currentProjectLogPath",
32 | HelpText = "Location to log the current project's properties and items",
33 | Default = "currentProject.log")]
34 | public string CurrentProjectLogPath { get; set; }
35 |
36 | [Option('b', "sdkBaseLineProjectLogPath",
37 | HelpText = "Location to log a sdk baseline project's properties and items",
38 | Default = "sdkBaseLineProject.log")]
39 | public string SdkBaseLineProjectLogPath { get; set; }
40 | }
41 |
42 | [Verb("diff", HelpText = "Diff a given project against a SDK baseline")]
43 | internal class DiffOptions : Options
44 | {
45 | [Option('d', "diffReportPath",
46 | HelpText = "Location to output a diff of the current project against a sdk baseline",
47 | Default = "report.diff")]
48 | public string DiffReportPath { get; set; }
49 | }
50 |
51 | [Verb("convert", HelpText = "Convert a given project to be based on the SDK")]
52 | internal class ConvertOptions : Options
53 | {
54 | [Option('o', "outputProjectPath",
55 | HelpText = "Location to output the converted project",
56 | Required = false)]
57 | public string OutputProjectPath { get; set; }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/ProjectSimplifier/MSBuildProjectRootElement.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Microsoft.Build.Construction;
3 |
4 | namespace ProjectSimplifier
5 | {
6 | public interface IProjectRootElement
7 | {
8 | string ToolsVersion { get; set; }
9 | string Sdk { get; set; }
10 | ICollection Imports { get; }
11 | ICollection ImportGroups { get; }
12 | ICollection PropertyGroups { get; }
13 | ICollection ItemGroups { get; }
14 |
15 | ProjectPropertyElement CreatePropertyElement(string propertyName);
16 | ProjectPropertyGroupElement AddPropertyGroup();
17 | ProjectItemGroupElement AddItemGroup();
18 |
19 | void Save(string path);
20 | void RemoveChild(ProjectElement child);
21 | void Reload(bool throwIfUnsavedChanges = true, bool? preserveFormatting = null);
22 | }
23 |
24 |
25 | internal class MSBuildProjectRootElement : IProjectRootElement
26 | {
27 | private readonly ProjectRootElement _rootElement;
28 |
29 | public MSBuildProjectRootElement(ProjectRootElement rootElement)
30 | {
31 | _rootElement = rootElement;
32 | }
33 |
34 | public string ToolsVersion { get => _rootElement.ToolsVersion; set => _rootElement.ToolsVersion = value; }
35 | public string Sdk { get => _rootElement.Sdk; set => _rootElement.Sdk = value; }
36 | public ICollection Imports => _rootElement.Imports;
37 | public ICollection ImportGroups => _rootElement.ImportGroups;
38 | public ICollection PropertyGroups => _rootElement.PropertyGroups;
39 | public ICollection ItemGroups => _rootElement.ItemGroups;
40 |
41 | public ProjectItemGroupElement AddItemGroup() => _rootElement.AddItemGroup();
42 |
43 | public ProjectPropertyGroupElement AddPropertyGroup() => _rootElement.AddPropertyGroup();
44 | public ProjectPropertyElement CreatePropertyElement(string name) => _rootElement.CreatePropertyElement(name);
45 |
46 | public void Reload(bool throwIfUnsavedChanges = true, bool? preserveFormatting = null) => _rootElement.Reload(throwIfUnsavedChanges, preserveFormatting);
47 | public void RemoveChild(ProjectElement child) => _rootElement.RemoveChild(child);
48 | public void Save(string path) => _rootElement.Save(path);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/ProjectSimplifier/Facts.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Collections.Immutable;
3 |
4 | namespace ProjectSimplifier
5 | {
6 | internal static class Facts
7 | {
8 | ///
9 | /// Props files which are known to be imported in standard projects created from templates that can be converted to use the SDK
10 | ///
11 | public static ImmutableArray PropsConvertibleToSDK => ImmutableArray.Create("Microsoft.Common.props");
12 |
13 | ///
14 | /// Targets files which are known to be imported in standard projects created from templates that can be converted to use the SDK.
15 | ///
16 | public static ImmutableArray TargetsConvertibleToSDK => ImmutableArray.Create(
17 | "Microsoft.CSharp.targets",
18 | "Microsoft.VisualBasic.targets",
19 | "Microsoft.Portable.CSharp.targets",
20 | "Microsoft.Portable.VisualBasic.targets");
21 |
22 | ///
23 | /// Mapping of PCL profiles to netstandard versions.
24 | ///
25 | public static ImmutableDictionary PCLToNetStandardVersionMapping => ImmutableDictionary.CreateRange(new Dictionary
26 | {
27 | { "Profile7", "1.1" },
28 | { "Profile31", "1.0" },
29 | { "Profile32", "1.2" },
30 | { "Profile44", "1.2" },
31 | { "Profile49", "1.0" },
32 | { "Profile78", "1.0" },
33 | { "Profile84", "1.0" },
34 | { "Profile111", "1.0" },
35 | { "Profile151", "1.0" },
36 | { "Profile157", "1.0" },
37 | { "Profile259", "1.0" },
38 | });
39 |
40 | public static ImmutableArray PropertiesNotNeededInCPS => ImmutableArray.Create(
41 | "ProjectGuid", // Guids are in-memory in CPS
42 | "ProjectTypeGuids", // Not used - capabilities are used instead
43 | "TargetFrameworkIdentifier", // Inferred from TargetFramework
44 | "TargetFrameworkVersion", // Inferred from TargetFramework
45 | "TargetFrameworkProfile" // Inferred from TargetFramework
46 | );
47 |
48 | public static ImmutableArray GlobbedItemTypes => ImmutableArray.Create(
49 | "Compile",
50 | "EmbeddedResource",
51 | "None"
52 | );
53 |
54 | public const string SharedProjectsImportLabel = "Shared";
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/ProjectSimplifier.Tests/ProjectExtensionsTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using ProjectSimplifier;
5 | using ProjectSimplifier.Tests.Mocks;
6 | using Xunit;
7 |
8 | namespace ProjectSimplifier.Tests
9 | {
10 | public class ProjectExtensionsTests
11 | {
12 | [Theory]
13 | [InlineData("net46", null, null, null, "net46")]
14 | [InlineData(null, ".NETFramework", "v4.6", null, "net4.6")]
15 | [InlineData(null, ".NETCoreApp", "v1.0", null, "netcoreapp1.0")]
16 | [InlineData(null, ".NETStandard", "v1.0", null, "netstandard1.0")]
17 | [InlineData(null, ".NETPortable", "v5.0", "Profile7", "netstandard1.1")]
18 | public void GetTargetFramework(string targetFramework, string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile, string expectedTargetFramework)
19 | {
20 | var properties = new Dictionary();
21 | if (targetFramework != null)
22 | properties.Add("TargetFramework", targetFramework);
23 | if (targetFrameworkIdentifier != null)
24 | properties.Add("TargetFrameworkIdentifier", targetFrameworkIdentifier);
25 | if (targetFrameworkVersion != null)
26 | properties.Add("TargetFrameworkVersion", targetFrameworkVersion);
27 | if (targetFrameworkProfile != null)
28 | properties.Add("TargetFrameworkProfile", targetFrameworkProfile);
29 |
30 | var project = IProjectFactory.Create(properties);
31 | var actualTargetFramework = ProjectExtensions.GetTargetFramework(project);
32 |
33 | Assert.Equal(expectedTargetFramework, actualTargetFramework);
34 | }
35 |
36 | [Theory]
37 | [InlineData(null, null, "v4.6")]
38 | [InlineData(null, ".NETCoreApp", null)]
39 | [InlineData(null, "Unknown", null)]
40 | public void GetTargetFramework_Throws(string targetFramework, string targetFrameworkIdentifier, string targetFrameworkVersion)
41 | {
42 | var properties = new Dictionary();
43 | if (targetFramework != null)
44 | properties.Add("TargetFramework", targetFramework);
45 | if (targetFrameworkIdentifier != null)
46 | properties.Add("TargetFrameworkIdentifier", targetFrameworkIdentifier);
47 | if (targetFrameworkVersion != null)
48 | properties.Add("TargetFrameworkVersion", targetFrameworkVersion);
49 |
50 | var project = IProjectFactory.Create(properties);
51 |
52 | Assert.Throws(() => ProjectExtensions.GetTargetFramework(project));
53 | }
54 |
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/ProjectSimplifier.Tests/PropertiesDiffTests.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 | using System.Linq;
3 | using ProjectSimplifier.Tests.Mocks;
4 | using Xunit;
5 |
6 | namespace ProjectSimplifier.Tests
7 | {
8 | public class PropertiesDiffTests
9 | {
10 | [Theory]
11 | [InlineData("A=B", "A", "A=B", "A", null, null)]
12 | [InlineData("A=B", "A", "D=E", null, "A", null)]
13 | [InlineData("A=B;C=D", "A", "C=D", null, "A", null)]
14 | [InlineData("A=B;C=D", "A", "A=C", null, null, "A")]
15 | [InlineData("A=B;C=D", "A;C", "C=E", null, "A", "C")]
16 | [InlineData("A=B;C=D;E=F", "A;C;E", "C=E;E=F", "E", "A", "C")]
17 | public void PropertiesDiff(string projectProps, string propsInFile, string sdkBaselineProps, string expectedDefaultedProps, string expectedNotDefaultedProps, string expectedChangedProps)
18 | {
19 | var project = IProjectFactory.Create(projectProps, propsInFile);
20 | var sdkBaselineProject = IProjectFactory.Create(sdkBaselineProps, propsInFile);
21 |
22 | var differ = new Differ(project, sdkBaselineProject);
23 |
24 | var diff = differ.GetPropertiesDiff();
25 |
26 | if (expectedDefaultedProps == null)
27 | {
28 | Assert.Empty(diff.DefaultedProperties);
29 | }
30 | else
31 | {
32 | Assert.Equal(diff.DefaultedProperties.Select(p=> p.Name), expectedDefaultedProps.Split(';'));
33 | }
34 |
35 | if (expectedNotDefaultedProps == null)
36 | {
37 | Assert.Empty(diff.NotDefaultedProperties);
38 | }
39 | else
40 | {
41 | Assert.Equal(diff.NotDefaultedProperties.Select(p => p.Name), expectedNotDefaultedProps.Split(';'));
42 | }
43 |
44 | if (expectedChangedProps == null)
45 | {
46 | Assert.Empty(diff.ChangedProperties);
47 | }
48 | else
49 | {
50 | Assert.Equal(diff.ChangedProperties.Select(p => p.oldProp.Name), expectedChangedProps.Split(';'));
51 | }
52 | }
53 |
54 | [Fact]
55 | public void PropertiesDiff_GetLines()
56 | {
57 | var defaultedProps = IProjectFactory.Create("A=B;C=D").Properties.ToImmutableArray();
58 | var removedProps = IProjectFactory.Create("E=F;G=H").Properties.ToImmutableArray();
59 | var changedProps = IProjectFactory.Create("I=J").Properties.Zip(IProjectFactory.Create("I=K").Properties, (a, b) => (a, b)).ToImmutableArray();
60 | var diff = new PropertiesDiff(defaultedProps, removedProps, changedProps);
61 |
62 | var lines = diff.GetDiffLines();
63 | var expectedLines = new[]
64 | {
65 | "Properties that are defaulted by the SDK:",
66 | "- A = B",
67 | "- C = D",
68 | "",
69 | "Properties that are not defaulted by the SDK:",
70 | "+ E = F",
71 | "+ G = H",
72 | "",
73 | "Properties whose value is different from the SDK's default:",
74 | "- I = J",
75 | "+ I = K",
76 | ""
77 | };
78 |
79 | Assert.Equal(expectedLines, lines);
80 | }
81 |
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/ProjectSimplifier/ProjectExtensions.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json.Linq;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 |
7 | namespace ProjectSimplifier
8 | {
9 | internal static class ProjectExtensions
10 | {
11 | public static void LogProjectProperties(this IProject project, string logFileName)
12 | {
13 | var lines = new List();
14 | foreach (var prop in project.Properties.OrderBy(p => p.Name))
15 | {
16 | lines.Add($"{prop.Name} = {prop.EvaluatedValue}");
17 | }
18 | File.WriteAllLines(logFileName, lines);
19 | }
20 |
21 | public static string GetTargetFramework(this IProject project)
22 | {
23 | var tf = project.GetPropertyValue("TargetFramework");
24 | if (!string.IsNullOrEmpty(tf))
25 | {
26 | return tf;
27 | }
28 |
29 | var tfi = project.GetPropertyValue("TargetFrameworkIdentifier");
30 | if (tfi == "")
31 | {
32 | throw new InvalidOperationException("TargetFrameworkIdentifier is not set!");
33 | }
34 |
35 | var tfv = project.GetPropertyValue("TargetFrameworkVersion");
36 |
37 | switch (tfi)
38 | {
39 | case ".NETFramework":
40 | tf = "net";
41 | break;
42 | case ".NETStandard":
43 | tf = "netstandard";
44 | break;
45 | case ".NETCoreApp":
46 | tf = "netcoreapp";
47 | break;
48 | case ".NETPortable":
49 | tf = "netstandard";
50 | break;
51 | default:
52 | throw new InvalidOperationException($"Unknown TargetFrameworkIdentifier {tfi}");
53 | }
54 |
55 | if (tfi == ".NETPortable")
56 | {
57 | var profile = project.GetPropertyValue("TargetFrameworkProfile");
58 |
59 | if (profile == string.Empty && tfv == "v5.0")
60 | {
61 | tf = GetTargetFrameworkFromProjectJson(project);
62 | }
63 | else
64 | {
65 | var netstandardVersion = Facts.PCLToNetStandardVersionMapping[profile];
66 | tf += netstandardVersion;
67 | }
68 | }
69 | else
70 | {
71 | if (tfv == "")
72 | {
73 | throw new InvalidOperationException("TargetFrameworkVersion is not set!");
74 | }
75 |
76 | tf += tfv.TrimStart('v');
77 | }
78 |
79 | return tf;
80 | }
81 |
82 | private static string GetTargetFrameworkFromProjectJson(IProject project)
83 | {
84 | string projectFolder = project.GetPropertyValue("MSBuildProjectDirectory");
85 | string projectJsonPath = Path.Combine(projectFolder, "project.json");
86 |
87 | string projectJsonContents = File.ReadAllText(projectJsonPath);
88 |
89 | JObject json = JObject.Parse(projectJsonContents);
90 |
91 | var frameworks = json["frameworks"];
92 | string tf = ((JProperty)frameworks.Single()).Name;
93 | return tf;
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/ProjectSimplifier.Tests/Mocks/IProjectFactory.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Microsoft.Build.Evaluation;
4 | using Moq;
5 |
6 | namespace ProjectSimplifier.Tests.Mocks
7 | {
8 | internal class IProjectFactory
9 | {
10 | ///
11 | /// Expected format here is "A=B;C=D"
12 | ///
13 | public static IProject Create(string projectProperties, string propertiesInFile="")
14 | {
15 | var lines = projectProperties.Split(';');
16 | return Create(lines.Select(p => p.Split('=')).ToDictionary(a => a[0], a => a[1]), propertiesInFile.Split(';'));
17 | }
18 |
19 | public static IProject Create(IEnumerable<(string ItemType, string[] Items)> items)
20 | {
21 | var mock = new Mock();
22 |
23 | var projectItems = new List();
24 |
25 | foreach (var itemGroup in items)
26 | {
27 | foreach (var item in itemGroup.Items)
28 | {
29 | var itemSplit = item.Split('|');
30 | var itemInclude = itemSplit.First();
31 | var metadata = itemSplit.Length > 1 ? itemSplit.Skip(1).Select(p => p.Split('=')).ToDictionary(a => a[0], a => a[1]) : new Dictionary();
32 | var metadataMocks = metadata?.Select(kvp =>
33 | {
34 | var metadataMock = new Mock();
35 | metadataMock.SetupGet(md => md.Name).Returns(kvp.Key);
36 | metadataMock.SetupGet(md => md.EvaluatedValue).Returns(kvp.Value);
37 | metadataMock.SetupGet(md => md.UnevaluatedValue).Returns(kvp.Value);
38 | return metadataMock.Object;
39 | });
40 |
41 | var projectItemMock = new Mock();
42 | projectItemMock.SetupGet(pi => pi.ItemType).Returns(itemGroup.ItemType);
43 | projectItemMock.SetupGet(pi => pi.EvaluatedInclude).Returns(itemInclude);
44 | projectItemMock.SetupGet(pi => pi.DirectMetadata).Returns(metadataMocks);
45 | projectItems.Add(projectItemMock.Object);
46 | }
47 | }
48 |
49 | mock.SetupGet(m => m.Items).Returns(projectItems);
50 |
51 | return mock.Object;
52 | }
53 |
54 | public static IProject Create(IDictionary projectProperties, IEnumerable propertiesInFile=null)
55 | {
56 | var mock = new Mock();
57 |
58 | mock.Setup(m => m.GetPropertyValue(It.IsAny())).Returns((string prop) => projectProperties.ContainsKey(prop) ? projectProperties[prop] : "");
59 |
60 | mock.Setup(m => m.GetProperty(It.IsAny())).Returns((string prop) =>
61 | {
62 | if (projectProperties.ContainsKey(prop))
63 | {
64 | return MockProperty(prop, projectProperties[prop], propertiesInFile?.Contains(prop));
65 | }
66 | return null;
67 | });
68 |
69 | mock.SetupGet(m => m.Properties).Returns(projectProperties.Select(kvp => MockProperty(kvp.Key, kvp.Value, propertiesInFile?.Contains(kvp.Key))).ToArray());
70 |
71 | return mock.Object;
72 | }
73 |
74 | private static IProjectProperty MockProperty(string propName, string propValue, bool? isDefinedInProject)
75 | {
76 | var projectProperty = new Mock();
77 | projectProperty.SetupGet(pp => pp.Name).Returns(propName);
78 | projectProperty.SetupGet(pp => pp.EvaluatedValue).Returns(propValue);
79 | projectProperty.SetupGet(pp => pp.IsDefinedInProject).Returns(isDefinedInProject??false);
80 | return projectProperty.Object;
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/ProjectSimplifier/MSBuildProject.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Microsoft.Build.Evaluation;
5 |
6 | namespace ProjectSimplifier
7 | {
8 | ///
9 | /// Interface used to Mock access to MSBuild's Project apis.
10 | ///
11 | public interface IProject
12 | {
13 | ICollection Properties { get; }
14 |
15 | ICollection Items { get; }
16 |
17 | IProjectProperty GetProperty(string name);
18 |
19 | string GetPropertyValue(string name);
20 | }
21 |
22 | public interface IProjectProperty
23 | {
24 | string Name { get; }
25 | string EvaluatedValue { get; }
26 | string UnevaluatedValue { get; }
27 | bool IsDefinedInProject { get; }
28 | }
29 |
30 | public interface IProjectItem
31 | {
32 | string ItemType { get; }
33 | string EvaluatedInclude { get; }
34 | IEnumerable DirectMetadata { get; }
35 | }
36 |
37 | public interface IProjectMetadata : IEquatable
38 | {
39 | string Name { get; }
40 | string UnevaluatedValue { get; }
41 | string EvaluatedValue { get; }
42 | }
43 |
44 | internal class MSBuildProjectProperty : IProjectProperty
45 | {
46 | private readonly ProjectProperty _property;
47 |
48 | public MSBuildProjectProperty(ProjectProperty property)
49 | {
50 | _property = property;
51 | }
52 |
53 | public string Name => _property.Name;
54 |
55 | public string EvaluatedValue => _property.EvaluatedValue;
56 |
57 | public string UnevaluatedValue => _property.UnevaluatedValue;
58 |
59 | public bool IsDefinedInProject => !_property.IsImported &&
60 | !_property.IsEnvironmentProperty &&
61 | !_property.IsGlobalProperty &&
62 | !_property.IsReservedProperty;
63 | }
64 |
65 | internal class MSBuildProjectItem : IProjectItem
66 | {
67 | private readonly ProjectItem _item;
68 |
69 | public MSBuildProjectItem(ProjectItem item)
70 | {
71 | _item = item;
72 | }
73 |
74 | public string ItemType => _item.ItemType;
75 |
76 | public string EvaluatedInclude => _item.EvaluatedInclude;
77 |
78 | public IEnumerable DirectMetadata => _item.DirectMetadata.Select(md => new MSBuildProjectMetadata(md));
79 | }
80 |
81 | internal class MSBuildProjectMetadata : IProjectMetadata
82 | {
83 | private readonly ProjectMetadata _projectMetadata;
84 |
85 | public MSBuildProjectMetadata(ProjectMetadata projectMetadata)
86 | {
87 | _projectMetadata = projectMetadata;
88 | }
89 |
90 | public string Name => _projectMetadata.Name;
91 |
92 | public string UnevaluatedValue => _projectMetadata.UnevaluatedValue;
93 |
94 | public string EvaluatedValue => _projectMetadata.EvaluatedValue;
95 |
96 | public bool Equals(IProjectMetadata other)
97 | {
98 | return _projectMetadata.Name.Equals(other.Name) &&
99 | _projectMetadata.UnevaluatedValue.Equals(other.UnevaluatedValue) &&
100 | _projectMetadata.EvaluatedValue.Equals(other.EvaluatedValue);
101 | }
102 | }
103 |
104 | internal class MSBuildProject : IProject
105 | {
106 | private readonly Project _project;
107 |
108 | public MSBuildProject(Project project) => _project = project ?? throw new ArgumentNullException(nameof(project));
109 |
110 | public ICollection Properties => _project.Properties.Select(p => new MSBuildProjectProperty(p)).ToArray();
111 |
112 | public ICollection Items => _project.Items.Select(i => new MSBuildProjectItem(i)).ToArray();
113 |
114 | public IProjectProperty GetProperty(string name) => _project.GetProperty(name) != null ? new MSBuildProjectProperty(_project.GetProperty(name)) : null;
115 |
116 | public string GetPropertyValue(string name) => _project.GetPropertyValue(name);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/ProjectSimplifier/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Reflection;
4 | using CommandLine;
5 |
6 | namespace ProjectSimplifier
7 | {
8 | class Program
9 | {
10 | static int Main(string[] args)
11 | {
12 | var options = Parser.Default.ParseArguments(args);
13 | switch (options)
14 | {
15 | case Parsed