From 362f71a94af1bdf5175a60c2db9b6df49816d17d Mon Sep 17 00:00:00 2001 From: Bryan Thornbury Date: Thu, 8 Sep 2016 14:40:46 -0700 Subject: [PATCH] Project Json mapping migration support --- .../TestAppWithContents/Program.cs | 2 +- .../TestAppWithContents/project.json | 31 +- .../TestAppWithContents/testcontentfile2.txt | 0 .../MSBuildExtensions.cs | 107 +++++- .../ProjectMigrator.cs | 9 +- .../Rules/MigrateBuildOptionsRule.cs | 67 +--- .../Rules/MigrateConfigurationsRule.cs | 52 ++- .../transforms/IncludeContextTransform.cs | 156 +++++--- .../transforms/TransformApplicator.cs | 137 +++++-- src/Microsoft.DotNet.ProjectModel/Project.cs | 8 +- .../Properties/AssemblyInfo.cs | 1 + .../GivenAProjectMigrator.cs | 4 +- .../GivenMSBuildExtensions.cs | 148 ++++++++ ...ft.DotNet.ProjectJsonMigration.Tests.xproj | 4 +- .../GivenThatIWantToMigrateBuildOptions.cs | 14 +- .../GivenThatIWantToMigrateConfigurations.cs | 359 +++++++++++++++++- .../GivenThatIWantToMigrateTestApps.cs | 12 + 17 files changed, 910 insertions(+), 201 deletions(-) create mode 100644 TestAssets/TestProjects/TestAppWithContents/testcontentfile2.txt diff --git a/TestAssets/TestProjects/TestAppWithContents/Program.cs b/TestAssets/TestProjects/TestAppWithContents/Program.cs index 28e184cb5..7ab8924f9 100644 --- a/TestAssets/TestProjects/TestAppWithContents/Program.cs +++ b/TestAssets/TestProjects/TestAppWithContents/Program.cs @@ -7,7 +7,7 @@ namespace ConsoleApplication public static int Main(string[] args) { Console.WriteLine("Hello World!"); - return 100; + return 0; } } } diff --git a/TestAssets/TestProjects/TestAppWithContents/project.json b/TestAssets/TestProjects/TestAppWithContents/project.json index c71fdca4f..9bc98a36a 100644 --- a/TestAssets/TestProjects/TestAppWithContents/project.json +++ b/TestAssets/TestProjects/TestAppWithContents/project.json @@ -3,29 +3,30 @@ "buildOptions": { "emitEntryPoint": true, "copyToOutput": { - "include": "testcontentfile.txt" + "include": "testcontentfile.txt", + "mappings": { + "dir/mappingfile.txt":{ + "include": "testcontentfile2.txt" + }, + "out/": { + "include": ["project.json", "Program.cs"], + "exclude": ["Program.cs"], + "includeFiles": ["Program.cs"], + "excludeFiles": ["Program.cs"] + } + } } }, "dependencies": { - "Microsoft.NETCore.App": "1.0.1" + "Microsoft.NETCore.App": { + "version": "1.0.1", + "type": "platform" + } }, "frameworks": { "netcoreapp1.0": {} }, "publishOptions": { "include": "testcontentfile.txt" - }, - "runtimes": { - "win7-x64": {}, - "win7-x86": {}, - "osx.10.10-x64": {}, - "osx.10.11-x64": {}, - "ubuntu.14.04-x64": {}, - "ubuntu.16.04-x64": {}, - "centos.7-x64": {}, - "rhel.7.2-x64": {}, - "debian.8-x64": {}, - "fedora.23-x64": {}, - "opensuse.13.2-x64": {} } } diff --git a/TestAssets/TestProjects/TestAppWithContents/testcontentfile2.txt b/TestAssets/TestProjects/TestAppWithContents/testcontentfile2.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/MSBuildExtensions.cs b/src/Microsoft.DotNet.ProjectJsonMigration/MSBuildExtensions.cs index 241092ec5..87c7a1161 100644 --- a/src/Microsoft.DotNet.ProjectJsonMigration/MSBuildExtensions.cs +++ b/src/Microsoft.DotNet.ProjectJsonMigration/MSBuildExtensions.cs @@ -15,6 +15,79 @@ namespace Microsoft.DotNet.ProjectJsonMigration { public static class MSBuildExtensions { + public static bool IsEquivalentTo(this ProjectItemElement item, ProjectItemElement otherItem) + { + // Different includes + if (item.IntersectIncludes(otherItem).Count() != item.Includes().Count()) + { + MigrationTrace.Instance.WriteLine("ms: includes"); + return false; + } + + // Different Excludes + if (item.IntersectExcludes(otherItem).Count() != item.Excludes().Count()) + { + MigrationTrace.Instance.WriteLine("ms: excludes"); + return false; + } + + // Different remove + if (item.Remove != otherItem.Remove) + { + MigrationTrace.Instance.WriteLine("ms: remove"); + return false; + } + + // Different Metadata + var metadataTuples = otherItem.Metadata.Select(m => Tuple.Create(m, item)).Concat( + item.Metadata.Select(m => Tuple.Create(m, otherItem))); + foreach (var metadataTuple in metadataTuples) + { + var metadata = metadataTuple.Item1; + var itemToCompare = metadataTuple.Item2; + + var otherMetadata = itemToCompare.GetMetadataWithName(metadata.Name); + if (otherMetadata == null) + { + MigrationTrace.Instance.WriteLine($"ms: metadata doesn't exist {{ {metadata.Name} {metadata.Value} }}"); + return false; + } + + if (!metadata.ValueEquals(otherMetadata)) + { + MigrationTrace.Instance.WriteLine("ms: metadata has another value {{ {metadata.Name} {metadata.Value} {otherMetadata.Value} }}"); + return false; + } + } + + return true; + } + + public static ISet ConditionChain(this ProjectElement projectElement) + { + var conditionChainSet = new HashSet(); + + if (!string.IsNullOrEmpty(projectElement.Condition)) + { + conditionChainSet.Add(projectElement.Condition); + } + + foreach (var parent in projectElement.AllParents) + { + if (!string.IsNullOrEmpty(parent.Condition)) + { + conditionChainSet.Add(parent.Condition); + } + } + + return conditionChainSet; + } + + public static bool ConditionChainsAreEquivalent(this ProjectElement projectElement, ProjectElement otherProjectElement) + { + return projectElement.ConditionChain().SetEquals(otherProjectElement.ConditionChain()); + } + public static IEnumerable PropertiesWithoutConditions( this ProjectRootElement projectRoot) { @@ -39,6 +112,12 @@ namespace Microsoft.DotNet.ProjectJsonMigration return SplitSemicolonDelimitedValues(item.Exclude); } + public static IEnumerable Removes( + this ProjectItemElement item) + { + return SplitSemicolonDelimitedValues(item.Remove); + } + public static IEnumerable AllConditions(this ProjectElement projectElement) { return new string[] { projectElement.Condition }.Concat(projectElement.AllParents.Select(p=> p.Condition)); @@ -49,6 +128,11 @@ namespace Microsoft.DotNet.ProjectJsonMigration return item.Includes().Intersect(otherItem.Includes()); } + public static IEnumerable IntersectExcludes(this ProjectItemElement item, ProjectItemElement otherItem) + { + return item.Excludes().Intersect(otherItem.Excludes()); + } + public static void RemoveIncludes(this ProjectItemElement item, IEnumerable includesToRemove) { item.Include = string.Join(";", item.Includes().Except(includesToRemove)); @@ -64,11 +148,6 @@ namespace Microsoft.DotNet.ProjectJsonMigration item.Exclude = string.Join(";", item.Excludes().Union(excludesToAdd)); } - public static ProjectMetadataElement GetMetadataWithName(this ProjectItemElement item, string name) - { - return item.Metadata.FirstOrDefault(m => m.Name.Equals(name, StringComparison.Ordinal)); - } - public static bool ValueEquals(this ProjectMetadataElement metadata, ProjectMetadataElement otherMetadata) { return metadata.Value.Equals(otherMetadata.Value, StringComparison.Ordinal); @@ -90,6 +169,24 @@ namespace Microsoft.DotNet.ProjectJsonMigration } } + public static ProjectMetadataElement GetMetadataWithName(this ProjectItemElement item, string name) + { + return item.Metadata.FirstOrDefault(m => m.Name.Equals(name, StringComparison.Ordinal)); + } + + public static bool HasConflictingMetadata(this ProjectItemElement item, ProjectItemElement otherItem) + { + foreach (var metadata in item.Metadata) + { + if (otherItem.Metadata.Any(m => m.Name == metadata.Name && m.Value != metadata.Value)) + { + return true; + } + } + + return false; + } + public static void AddMetadata(this ProjectItemElement item, ProjectMetadataElement metadata) { var existingMetadata = item.GetMetadataWithName(metadata.Name); diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/ProjectMigrator.cs b/src/Microsoft.DotNet.ProjectJsonMigration/ProjectMigrator.cs index c1d2bbc32..14b9d84e9 100644 --- a/src/Microsoft.DotNet.ProjectJsonMigration/ProjectMigrator.cs +++ b/src/Microsoft.DotNet.ProjectJsonMigration/ProjectMigrator.cs @@ -18,13 +18,11 @@ namespace Microsoft.DotNet.ProjectJsonMigration public class ProjectMigrator { // TODO: Migrate PackOptions - // TODO: Support Mappings in IncludeContext Transformations // TODO: Migrate Multi-TFM projects // TODO: Tests // TODO: Out of Scope // - Globs that resolve to directories: /some/path/**/somedir // - Migrating Deprecated project.jsons - // - Configuration dependent source exclusion private readonly IMigrationRule _ruleSet; @@ -85,7 +83,7 @@ namespace Microsoft.DotNet.ProjectJsonMigration if (diagnostics.Any()) { MigrationErrorCodes.MIGRATE1011( - $"{projectDirectory}{Environment.NewLine}{string.Join(Environment.NewLine, diagnostics.Select(d => d.Message))}") + $"{projectDirectory}{Environment.NewLine}{string.Join(Environment.NewLine, diagnostics.Select(d => FormatDiagnosticMessage(d)))}") .Throw(); } @@ -99,6 +97,11 @@ namespace Microsoft.DotNet.ProjectJsonMigration } } + private string FormatDiagnosticMessage(DiagnosticMessage d) + { + return $"{d.Message} (line: {d.StartLine}, file: {d.SourceFilePath})"; + } + private void SetupOutputDirectory(string projectDirectory, string outputDirectory) { if (!Directory.Exists(outputDirectory)) diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateBuildOptionsRule.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateBuildOptionsRule.cs index cca260cd6..bd8a5831e 100644 --- a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateBuildOptionsRule.cs +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateBuildOptionsRule.cs @@ -23,7 +23,7 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Rules new AddPropertyTransform("OutputType", "Exe", compilerOptions => compilerOptions.EmitEntryPoint != null && compilerOptions.EmitEntryPoint.Value), new AddPropertyTransform("OutputType", "Library", - compilerOptions => compilerOptions.EmitEntryPoint == null || !compilerOptions.EmitEntryPoint.Value) + compilerOptions => compilerOptions.EmitEntryPoint != null && !compilerOptions.EmitEntryPoint.Value) }; private AddPropertyTransform[] KeyFileTransforms @@ -39,12 +39,12 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Rules private AddPropertyTransform DefineTransform => new AddPropertyTransform( "DefineConstants", - compilerOptions => string.Join(";", compilerOptions.Defines), + compilerOptions => "$(DefineConstants);" + string.Join(";", compilerOptions.Defines), compilerOptions => compilerOptions.Defines != null && compilerOptions.Defines.Any()); private AddPropertyTransform NoWarnTransform => new AddPropertyTransform( "NoWarn", - compilerOptions => string.Join(";", compilerOptions.SuppressWarnings), + compilerOptions => "$(NoWarn);" + string.Join(";", compilerOptions.SuppressWarnings), compilerOptions => compilerOptions.SuppressWarnings != null && compilerOptions.SuppressWarnings.Any()); private AddPropertyTransform PreserveCompilationContextTransform => @@ -129,11 +129,10 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Rules private Func> CopyToOutputFilesTransformExecute => (compilerOptions, projectDirectory) => CopyToOutputFilesTransform.Transform(GetCopyToOutputIncludeContext(compilerOptions, projectDirectory)); - - private readonly string _configuration; - private readonly NuGetFramework _framework; + private readonly ProjectPropertyGroupElement _configurationPropertyGroup; private readonly ProjectItemGroupElement _configurationItemGroup; + private readonly CommonCompilerOptions _configurationBuildOptions; private List> _propertyTransforms; private List>> _includeContextTransformExecutes; @@ -147,14 +146,12 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Rules } public MigrateBuildOptionsRule( - string configuration, - NuGetFramework framework, + CommonCompilerOptions configurationBuildOptions, ProjectPropertyGroupElement configurationPropertyGroup, ProjectItemGroupElement configurationItemGroup, ITransformApplicator transformApplicator = null) { - _configuration = configuration; - _framework = framework; + _configurationBuildOptions = configurationBuildOptions; _configurationPropertyGroup = configurationPropertyGroup; _configurationItemGroup = configurationItemGroup; _transformApplicator = transformApplicator ?? new TransformApplicator(); @@ -201,13 +198,11 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Rules var propertyGroup = _configurationPropertyGroup ?? migrationRuleInputs.CommonPropertyGroup; var itemGroup = _configurationItemGroup ?? migrationRuleInputs.CommonItemGroup; - var compilerOptions = projectContext.ProjectFile.GetCompilerOptions(projectContext.TargetFramework, null); - var configurationCompilerOptions = - projectContext.ProjectFile.GetCompilerOptions(_framework, _configuration); + var compilerOptions = projectContext.ProjectFile.GetCompilerOptions(null, null); // If we're in a configuration, we need to be careful not to overwrite values from BuildOptions // without a configuration - if (_configuration == null) + if (_configurationBuildOptions == null) { CleanExistingProperties(csproj); @@ -222,7 +217,7 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Rules { PerformConfigurationPropertyAndItemMappings( compilerOptions, - configurationCompilerOptions, + _configurationBuildOptions, propertyGroup, itemGroup, _transformApplicator, @@ -254,47 +249,7 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Rules var nonConfigurationOutput = includeContextTransformExecute(compilerOptions, projectDirectory); var configurationOutput = includeContextTransformExecute(configurationCompilerOptions, projectDirectory).ToArray(); - if (configurationOutput != null && nonConfigurationOutput != null) - { - // TODO: HACK: this is leaky, see top comments, the throw at least covers the scenario - ThrowIfConfigurationHasAdditionalExcludes(configurationOutput, nonConfigurationOutput); - RemoveCommonIncludes(configurationOutput, nonConfigurationOutput); - configurationOutput = configurationOutput.Where(i => i != null && !string.IsNullOrEmpty(i.Include)).ToArray(); - } - - // Don't merge with existing items when doing a configuration - transformApplicator.Execute(configurationOutput, itemGroup, mergeExisting: false); - } - } - - private void ThrowIfConfigurationHasAdditionalExcludes(IEnumerable configurationOutput, IEnumerable nonConfigurationOutput) - { - foreach (var item1 in configurationOutput) - { - if (item1 == null) - { - continue; - } - - var item2Excludes = new HashSet(); - foreach (var item2 in nonConfigurationOutput) - { - if (item2 != null) - { - item2Excludes.UnionWith(item2.Excludes()); - } - } - var configurationHasAdditionalExclude = - item1.Excludes().Any(exclude => item2Excludes.All(item2Exclude => item2Exclude != exclude)); - - if (configurationHasAdditionalExclude) - { - MigrationTrace.Instance.WriteLine(item1.Exclude); - MigrationTrace.Instance.WriteLine(item2Excludes.ToString()); - - MigrationErrorCodes.MIGRATE20012("Unable to migrate projects with excluded files in configurations.") - .Throw(); - } + transformApplicator.Execute(configurationOutput, itemGroup, mergeExisting: true); } } diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateConfigurationsRule.cs b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateConfigurationsRule.cs index 7bcef379a..e5695f098 100644 --- a/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateConfigurationsRule.cs +++ b/src/Microsoft.DotNet.ProjectJsonMigration/Rules/MigrateConfigurationsRule.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Build.Construction; using NuGet.Frameworks; +using Microsoft.DotNet.ProjectModel; namespace Microsoft.DotNet.ProjectJsonMigration.Rules { @@ -13,56 +14,75 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Rules { public void Apply(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs) { + MigrationTrace.Instance.WriteLine($"Executing rule: {nameof(MigrateConfigurationsRule)}"); var projectContext = migrationRuleInputs.DefaultProjectContext; var configurations = projectContext.ProjectFile.GetConfigurations().ToList(); var frameworks = new List(); - frameworks.Add(null); frameworks.AddRange(projectContext.ProjectFile.GetTargetFrameworks().Select(t => t.FrameworkName)); - if (!configurations.Any()) + if (!configurations.Any() && !frameworks.Any()) { return; } - var frameworkConfigurationCombinations = frameworks.SelectMany(f => configurations, Tuple.Create); - - foreach (var entry in frameworkConfigurationCombinations) + foreach (var framework in frameworks) { - var framework = entry.Item1; - var configuration = entry.Item2; + MigrateConfiguration(projectContext.ProjectFile, framework, migrationSettings, migrationRuleInputs); + } - MigrateConfiguration(configuration, framework, migrationSettings, migrationRuleInputs); + foreach (var configuration in configurations) + { + MigrateConfiguration(projectContext.ProjectFile, configuration, migrationSettings, migrationRuleInputs); } } private void MigrateConfiguration( + Project project, string configuration, - NuGetFramework framework, MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs) + { + var buildOptions = project.GetRawCompilerOptions(configuration); + var configurationCondition = $" '$(Configuration)' == '{configuration}' "; + + MigrateConfiguration(buildOptions, configurationCondition, migrationSettings, migrationRuleInputs); + } + + private void MigrateConfiguration( + Project project, + NuGetFramework framework, + MigrationSettings migrationSettings, + MigrationRuleInputs migrationRuleInputs) + { + var buildOptions = project.GetRawCompilerOptions(framework); + var configurationCondition = $" '$(TargetFrameworkIdentifier),Version=$(TargetFrameworkVersion)' == '{framework.DotNetFrameworkName}' "; + + MigrateConfiguration(buildOptions, configurationCondition, migrationSettings, migrationRuleInputs); + } + + private void MigrateConfiguration( + CommonCompilerOptions buildOptions, + string configurationCondition, + MigrationSettings migrationSettings, + MigrationRuleInputs migrationRuleInputs) { var csproj = migrationRuleInputs.OutputMSBuildProject; var propertyGroup = CreatePropertyGroupAtEndOfProject(csproj); var itemGroup = CreateItemGroupAtEndOfProject(csproj); - var configurationCondition = $" '$(Configuration)' == '{configuration}' "; - if (framework != null) - { - configurationCondition += - $" and '$(TargetFrameworkIdentifier),Version=$(TargetFrameworkVersion)' == '{framework.DotNetFrameworkName}' "; - } propertyGroup.Condition = configurationCondition; itemGroup.Condition = configurationCondition; - new MigrateBuildOptionsRule(configuration, framework, propertyGroup, itemGroup) + new MigrateBuildOptionsRule(buildOptions, propertyGroup, itemGroup) .Apply(migrationSettings, migrationRuleInputs); propertyGroup.RemoveIfEmpty(); itemGroup.RemoveIfEmpty(); } + private ProjectPropertyGroupElement CreatePropertyGroupAtEndOfProject(ProjectRootElement csproj) { var propertyGroup = csproj.CreatePropertyGroupElement(); diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/transforms/IncludeContextTransform.cs b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/IncludeContextTransform.cs index 0fd5e43fc..75120cefc 100644 --- a/src/Microsoft.DotNet.ProjectJsonMigration/transforms/IncludeContextTransform.cs +++ b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/IncludeContextTransform.cs @@ -7,19 +7,57 @@ using System.Threading.Tasks; using Microsoft.Build.Construction; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.ProjectJsonMigration.Models; +using Microsoft.DotNet.Tools.Common; namespace Microsoft.DotNet.ProjectJsonMigration.Transforms { public class IncludeContextTransform : ConditionalTransform> { - // TODO: If a directory is specified in project.json does this need to be replaced with a glob in msbuild? - // - Partially solved, what if the resolved glob is a directory? - // TODO: Support mappings + private Func> IncludeFilesExcludeFilesTransformGetter => + (itemName) => + new AddItemTransform( + itemName, + includeContext => FormatGlobPatternsForMsbuild(includeContext.IncludeFiles, includeContext.SourceBasePath), + includeContext => FormatGlobPatternsForMsbuild(includeContext.ExcludeFiles, includeContext.SourceBasePath), + includeContext => includeContext != null && includeContext.IncludeFiles.Count > 0); + + private Func> IncludeExcludeTransformGetter => + (itemName) => new AddItemTransform( + itemName, + includeContext => + { + var fullIncludeSet = includeContext.IncludePatterns.OrEmptyIfNull() + .Union(includeContext.BuiltInsInclude.OrEmptyIfNull()); + + return FormatGlobPatternsForMsbuild(fullIncludeSet, includeContext.SourceBasePath); + }, + includeContext => + { + var fullExcludeSet = includeContext.ExcludePatterns.OrEmptyIfNull() + .Union(includeContext.BuiltInsExclude.OrEmptyIfNull()) + .Union(includeContext.ExcludeFiles.OrEmptyIfNull()); + + return FormatGlobPatternsForMsbuild(fullExcludeSet, includeContext.SourceBasePath); + }, + includeContext => + { + return includeContext != null && + ( + (includeContext.IncludePatterns != null && includeContext.IncludePatterns.Count > 0) + || + (includeContext.BuiltInsInclude != null && includeContext.BuiltInsInclude.Count > 0) + ); + }); + + private Func> MappingsIncludeFilesExcludeFilesTransformGetter => + (itemName, targetPath) => AddMappingToTransform(IncludeFilesExcludeFilesTransformGetter(itemName), targetPath); + + private Func> MappingsIncludeExcludeTransformGetter => + (itemName, targetPath) => AddMappingToTransform(IncludeExcludeTransformGetter(itemName), targetPath); private readonly string _itemName; private bool _transformMappings; private readonly List> _metadata = new List>(); - private AddItemTransform[] _transformSet; public IncludeContextTransform( string itemName, @@ -42,55 +80,55 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Transforms return this; } - private void CreateTransformSet() + private IEnumerable, IncludeContext>> CreateTransformSet(IncludeContext source) { - var includeFilesExcludeFilesTransformation = new AddItemTransform( - _itemName, - includeContext => FormatPatterns(includeContext.IncludeFiles, includeContext.SourceBasePath), - includeContext => FormatPatterns(includeContext.ExcludeFiles, includeContext.SourceBasePath), - includeContext => includeContext != null && includeContext.IncludeFiles.Count > 0); - - var includeExcludeTransformation = new AddItemTransform( - _itemName, - includeContext => - { - var fullIncludeSet = includeContext.IncludePatterns.OrEmptyIfNull() - .Union(includeContext.BuiltInsInclude.OrEmptyIfNull()); - - return FormatPatterns(fullIncludeSet, includeContext.SourceBasePath); - }, - includeContext => - { - var fullExcludeSet = includeContext.ExcludePatterns.OrEmptyIfNull() - .Union(includeContext.BuiltInsExclude.OrEmptyIfNull()) - .Union(includeContext.ExcludeFiles.OrEmptyIfNull()); - - return FormatPatterns(fullExcludeSet, includeContext.SourceBasePath); - }, - includeContext => - { - return includeContext != null && - ( - (includeContext.IncludePatterns != null && includeContext.IncludePatterns.Count > 0) - || - (includeContext.BuiltInsInclude != null && includeContext.BuiltInsInclude.Count > 0) - ); - }); - - foreach (var metadata in _metadata) + var transformSet = new List, IncludeContext>> { - includeFilesExcludeFilesTransformation.WithMetadata(metadata); - includeExcludeTransformation.WithMetadata(metadata); + Tuple.Create(IncludeFilesExcludeFilesTransformGetter(_itemName), source), + Tuple.Create(IncludeExcludeTransformGetter(_itemName), source) + }; + + if (source == null) + { + return transformSet; + } + + // Mappings must be executed before the transform set to prevent a the + // non-mapped items that will merge with mapped items from being encompassed + foreach (var mappingEntry in source.Mappings.OrEmptyIfNull()) + { + var targetPath = mappingEntry.Key; + var includeContext = mappingEntry.Value; + + transformSet.Insert(0, + Tuple.Create( + MappingsIncludeExcludeTransformGetter(_itemName, targetPath), + includeContext)); + + transformSet.Insert(0, + Tuple.Create( + MappingsIncludeFilesExcludeFilesTransformGetter(_itemName, targetPath), + includeContext)); } - _transformSet = new [] + foreach (var metadataElement in _metadata) { - includeFilesExcludeFilesTransformation, - includeExcludeTransformation - }; + foreach (var transform in transformSet) + { + transform.Item1.WithMetadata(metadataElement); + } + } + + return transformSet; } - private string FormatPatterns(IEnumerable patterns, string projectDirectory) + public override IEnumerable ConditionallyTransform(IncludeContext source) + { + var transformSet = CreateTransformSet(source); + return transformSet.Select(t => t.Item1.Transform(t.Item2)); + } + + private string FormatGlobPatternsForMsbuild(IEnumerable patterns, string projectDirectory) { List mutatedPatterns = new List(patterns.Count()); @@ -121,6 +159,16 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Transforms } } + private AddItemTransform AddMappingToTransform( + AddItemTransform addItemTransform, + string targetPath) + { + var targetIsFile = MappingsTargetPathIsFile(targetPath); + var msbuildLinkMetadataValue = ConvertTargetPathToMsbuildMetadata(targetPath, targetIsFile); + + return addItemTransform.WithMetadata("Link", msbuildLinkMetadataValue); + } + private bool PatternIsDirectory(string pattern, string projectDirectory) { // TODO: what about /some/path/**/somedir? @@ -135,11 +183,21 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Transforms return Directory.Exists(path); } - public override IEnumerable ConditionallyTransform(IncludeContext source) + private string ConvertTargetPathToMsbuildMetadata(string targetPath, bool targetIsFile) { - CreateTransformSet(); + if (targetIsFile) + { + return targetPath; + } - return _transformSet.Select(t => t.Transform(source)); + return $"{targetPath}%(FileName)%(Extension)"; + } + + private bool MappingsTargetPathIsFile(string targetPath) + { + var normalizedTargetPath = PathUtility.GetPathWithDirectorySeparator(targetPath); + + return normalizedTargetPath[normalizedTargetPath.Length - 1] != Path.DirectorySeparatorChar; } } } diff --git a/src/Microsoft.DotNet.ProjectJsonMigration/transforms/TransformApplicator.cs b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/TransformApplicator.cs index 97280eaa2..7af6ddbac 100644 --- a/src/Microsoft.DotNet.ProjectJsonMigration/transforms/TransformApplicator.cs +++ b/src/Microsoft.DotNet.ProjectJsonMigration/transforms/TransformApplicator.cs @@ -66,34 +66,109 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Transforms return; } + MigrationTrace.Instance.WriteLine($"{nameof(TransformApplicator)}: Item {{ ItemType: {item.ItemType}, Condition: {item.Condition}, Include: {item.Include}, Exclude: {item.Exclude} }}"); + MigrationTrace.Instance.WriteLine($"{nameof(TransformApplicator)}: ItemGroup {{ Condition: {destinationItemGroup.Condition} }}"); + if (mergeExisting) { - var existingItems = FindExistingItems(item, destinationItemGroup.ContainingProject); + item = MergeWithExistingItemsWithSameCondition(item, destinationItemGroup); - foreach (var existingItem in existingItems) + // Item will be null when it's entire set of includes has been merged. + if (item == null) { - var mergeResult = MergeItems(item, existingItem); - item = mergeResult.InputItem; - - // Existing Item is null when it's entire set of includes has been merged with the MergeItem - if (mergeResult.ExistingItem == null) - { - existingItem.Parent.RemoveChild(existingItem); - } - - Execute(mergeResult.MergedItem, destinationItemGroup); + MigrationTrace.Instance.WriteLine($"{nameof(TransformApplicator)}: Item completely merged"); + return; } - // Item will be null only when it's entire set of includes is merged with existing items - if (item != null) + item = MergeWithExistingItemsWithDifferentCondition(item, destinationItemGroup); + + // Item will be null when it is equivalent to a conditionless item + if (item == null) { - Execute(item, destinationItemGroup); + MigrationTrace.Instance.WriteLine($"{nameof(TransformApplicator)}: Item c"); + return; } } - else + + Execute(item, destinationItemGroup); + } + + private ProjectItemElement MergeWithExistingItemsWithDifferentCondition(ProjectItemElement item, ProjectItemGroupElement destinationItemGroup) + { + var existingItemsWithDifferentCondition = + FindExistingItemsWithDifferentCondition(item, destinationItemGroup.ContainingProject, destinationItemGroup); + + MigrationTrace.Instance.WriteLine($"{nameof(TransformApplicator)}: Merging Item with {existingItemsWithDifferentCondition.Count()} existing items with a different condition chain."); + + foreach (var existingItem in existingItemsWithDifferentCondition) { - Execute(item, destinationItemGroup); + // When the existing item encompasses this item and it's condition is empty, ignore the current item + if (item.IsEquivalentTo(existingItem)) + { + MigrationTrace.Instance.WriteLine($"{nameof(TransformApplicator)}: equivalent {existingItem.ConditionChain().Count()}"); + + if (existingItem.ConditionChain().Count() == 0) + { + MigrationTrace.Instance.WriteLine($"{nameof(TransformApplicator)}: Ignoring Item {{ ItemType: {existingItem.ItemType}, Condition: {existingItem.Condition}, Include: {existingItem.Include}, Exclude: {existingItem.Exclude} }}"); + return null; + } + } } + + // If we haven't returned, and there are existing items with a separate condition, we need to + // overwrite with those items inside the destinationItemGroup by using a Remove + // Unless this is a conditionless item, in which case this the conditioned items should be doing the + // overwriting. + if (existingItemsWithDifferentCondition.Any() && + (item.ConditionChain().Count() > 0 || destinationItemGroup.ConditionChain().Count() > 0)) + { + // Merge with the first remove if possible + var existingRemoveItem = destinationItemGroup.Items + .Where(i => + string.IsNullOrEmpty(i.Include) + && string.IsNullOrEmpty(i.Exclude) + && !string.IsNullOrEmpty(i.Remove)) + .FirstOrDefault(); + + if (existingRemoveItem != null) + { + existingRemoveItem.Remove += ";" + item.Include; + } + else + { + var clearPreviousItem = _projectElementGenerator.CreateItemElement(item.ItemType); + clearPreviousItem.Remove = item.Include; + + Execute(clearPreviousItem, destinationItemGroup); + } + } + + return item; + } + + private ProjectItemElement MergeWithExistingItemsWithSameCondition(ProjectItemElement item, ProjectItemGroupElement destinationItemGroup) + { + var existingItemsWithSameCondition = + FindExistingItemsWithSameCondition(item, destinationItemGroup.ContainingProject, destinationItemGroup); + + MigrationTrace.Instance.WriteLine($"{nameof(TransformApplicator)}: Merging Item with {existingItemsWithSameCondition.Count()} existing items with the same condition chain."); + + foreach (var existingItem in existingItemsWithSameCondition) + { + var mergeResult = MergeItems(item, existingItem); + item = mergeResult.InputItem; + + // Existing Item is null when it's entire set of includes has been merged with the MergeItem + if (mergeResult.ExistingItem == null) + { + existingItem.Parent.RemoveChild(existingItem); + } + + MigrationTrace.Instance.WriteLine($"{nameof(TransformApplicator)}: Adding Merged Item {{ ItemType: {mergeResult.MergedItem.ItemType}, Condition: {mergeResult.MergedItem.Condition}, Include: {mergeResult.MergedItem.Include}, Exclude: {mergeResult.MergedItem.Exclude} }}"); + Execute(mergeResult.MergedItem, destinationItemGroup); + } + + return item; } public void Execute( @@ -132,9 +207,6 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Transforms } var commonIncludes = item.IntersectIncludes(existingItem).ToList(); - item.RemoveIncludes(commonIncludes); - existingItem.RemoveIncludes(commonIncludes); - var mergedItem = _projectElementGenerator.AddItem(item.ItemType, string.Join(";", commonIncludes)); mergedItem.UnionExcludes(existingItem.Excludes()); @@ -143,6 +215,9 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Transforms mergedItem.AddMetadata(existingItem.Metadata); mergedItem.AddMetadata(item.Metadata); + item.RemoveIncludes(commonIncludes); + existingItem.RemoveIncludes(commonIncludes); + var mergeResult = new MergeResult { InputItem = string.IsNullOrEmpty(item.Include) ? null : item, @@ -153,13 +228,29 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Transforms return mergeResult; } - private IEnumerable FindExistingItems(ProjectItemElement item, ProjectRootElement project) + private IEnumerable FindExistingItemsWithSameCondition( + ProjectItemElement item, + ProjectRootElement project, + ProjectElementContainer destinationContainer) { - return project.ItemsWithoutConditions() - .Where(i => string.Equals(i.ItemType, item.ItemType, StringComparison.Ordinal)) + return project.Items + .Where(i => i.Condition == item.Condition) + .Where(i => i.Parent.ConditionChainsAreEquivalent(destinationContainer)) + .Where(i => i.ItemType == item.ItemType) .Where(i => i.IntersectIncludes(item).Any()); } + private IEnumerable FindExistingItemsWithDifferentCondition( + ProjectItemElement item, + ProjectRootElement project, + ProjectElementContainer destinationContainer) + { + return project.Items + .Where(i => !i.ConditionChainsAreEquivalent(item) || !i.Parent.ConditionChainsAreEquivalent(destinationContainer)) + .Where(i => i.ItemType == item.ItemType) + .Where(i => i.IntersectIncludes(item).Any()); + } + private class MergeResult { public ProjectItemElement InputItem { get; set; } diff --git a/src/Microsoft.DotNet.ProjectModel/Project.cs b/src/Microsoft.DotNet.ProjectModel/Project.cs index 3aba2a6ac..c248e8a8d 100644 --- a/src/Microsoft.DotNet.ProjectModel/Project.cs +++ b/src/Microsoft.DotNet.ProjectModel/Project.cs @@ -98,8 +98,8 @@ namespace Microsoft.DotNet.ProjectModel { // Get all project options and combine them var rootOptions = GetCompilerOptions(); - var configurationOptions = configurationName != null ? GetCompilerOptions(configurationName) : null; - var targetFrameworkOptions = targetFramework != null ? GetCompilerOptions(targetFramework) : null; + var configurationOptions = configurationName != null ? GetRawCompilerOptions(configurationName) : null; + var targetFrameworkOptions = targetFramework != null ? GetRawCompilerOptions(targetFramework) : null; // Combine all of the options var compilerOptions = CommonCompilerOptions.Combine(rootOptions, configurationOptions, targetFrameworkOptions); @@ -136,7 +136,7 @@ namespace Microsoft.DotNet.ProjectModel return _defaultCompilerOptions; } - private CommonCompilerOptions GetCompilerOptions(string configurationName) + internal CommonCompilerOptions GetRawCompilerOptions(string configurationName) { CommonCompilerOptions options; if (_compilerOptionsByConfiguration.TryGetValue(configurationName, out options)) @@ -147,7 +147,7 @@ namespace Microsoft.DotNet.ProjectModel return null; } - private CommonCompilerOptions GetCompilerOptions(NuGetFramework frameworkName) + internal CommonCompilerOptions GetRawCompilerOptions(NuGetFramework frameworkName) { return GetTargetFramework(frameworkName)?.CompilerOptions; } diff --git a/src/Microsoft.DotNet.ProjectModel/Properties/AssemblyInfo.cs b/src/Microsoft.DotNet.ProjectModel/Properties/AssemblyInfo.cs index 8004866db..f0ea358d0 100644 --- a/src/Microsoft.DotNet.ProjectModel/Properties/AssemblyInfo.cs +++ b/src/Microsoft.DotNet.ProjectModel/Properties/AssemblyInfo.cs @@ -26,6 +26,7 @@ using System.Runtime.InteropServices; [assembly: Guid("303677d5-7312-4c3f-baee-beb1a9bd9fe6")] [assembly: AssemblyMetadataAttribute("Serviceable", "True")] +[assembly: InternalsVisibleTo("Microsoft.DotNet.ProjectJsonMigration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.DotNet.ProjectModel.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100039ac461fa5c82c7dd2557400c4fd4e9dcdf7ac47e3d572548c04cd4673e004916610f4ea5cbf86f2b1ca1cb824f2a7b3976afecfcf4eb72d9a899aa6786effa10c30399e6580ed848231fec48374e41b3acf8811931343fc2f73acf72dae745adbcb7063cc4b50550618383202875223fc75401351cd89c44bf9b50e7fa3796")] [assembly: InternalsVisibleTo( diff --git a/test/Microsoft.DotNet.ProjectJsonMigration.Tests/GivenAProjectMigrator.cs b/test/Microsoft.DotNet.ProjectJsonMigration.Tests/GivenAProjectMigrator.cs index 8b42c70bd..d053751fb 100644 --- a/test/Microsoft.DotNet.ProjectJsonMigration.Tests/GivenAProjectMigrator.cs +++ b/test/Microsoft.DotNet.ProjectJsonMigration.Tests/GivenAProjectMigrator.cs @@ -51,8 +51,8 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Tests migrateAction.ShouldThrow().Where( e => e.Message.Contains("MIGRATE1011::Deprecated Project:") - && e.Message.Contains("The 'packInclude' option is deprecated. Use 'files' in 'packOptions' instead.") - && e.Message.Contains("The 'compilationOptions' option is deprecated. Use 'buildOptions' instead.")); + && e.Message.Contains("The 'packInclude' option is deprecated. Use 'files' in 'packOptions' instead. (line: 6, file:") + && e.Message.Contains("The 'compilationOptions' option is deprecated. Use 'buildOptions' instead. (line: 3, file:")); } [Fact] diff --git a/test/Microsoft.DotNet.ProjectJsonMigration.Tests/GivenMSBuildExtensions.cs b/test/Microsoft.DotNet.ProjectJsonMigration.Tests/GivenMSBuildExtensions.cs index b93ff6778..f3281cd7e 100644 --- a/test/Microsoft.DotNet.ProjectJsonMigration.Tests/GivenMSBuildExtensions.cs +++ b/test/Microsoft.DotNet.ProjectJsonMigration.Tests/GivenMSBuildExtensions.cs @@ -7,6 +7,154 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Tests { public class GivenMSBuildExtensions { + [Fact] + public void ConditionChain_is_empty_when_element_and_parents_have_no_condition() + { + var project = ProjectRootElement.Create(); + var itemGroup = project.AddItemGroup(); + + var item1 = itemGroup.AddItem("test", "include1"); + + item1.ConditionChain().Should().HaveCount(0); + } + + [Fact] + public void ConditionChain_has_parent_conditions_when_element_is_empty() + { + var project = ProjectRootElement.Create(); + var itemGroup = project.AddItemGroup(); + itemGroup.Condition = "condition"; + + var item1 = itemGroup.AddItem("test", "include1"); + + item1.ConditionChain().Should().HaveCount(1); + item1.ConditionChain().First().Should().Be("condition"); + } + + [Fact] + public void ConditionChain_has_element_and_parent_conditions_when_they_exist() + { + var project = ProjectRootElement.Create(); + var itemGroup = project.AddItemGroup(); + itemGroup.Condition = "itemGroup"; + + var item1 = itemGroup.AddItem("test", "include1"); + item1.Condition = "item"; + + item1.ConditionChain().Should().HaveCount(2); + item1.ConditionChain().Should().BeEquivalentTo("itemGroup", "item"); + } + + [Fact] + public void ConditionChainsAreEquivalent_is_true_when_neither_element_or_parents_have_conditions() + { + var project = ProjectRootElement.Create(); + var itemGroup = project.AddItemGroup(); + + var item1 = itemGroup.AddItem("test", "include1"); + var item2 = itemGroup.AddItem("test", "include2"); + + item1.ConditionChainsAreEquivalent(item2).Should().BeTrue(); + } + + [Fact] + public void ConditionChainsAreEquivalent_is_true_when_elements_have_the_same_condition() + { + var project = ProjectRootElement.Create(); + var itemGroup = project.AddItemGroup(); + + var item1 = itemGroup.AddItem("test", "include1"); + var item2 = itemGroup.AddItem("test", "include2"); + item1.Condition = "item"; + item2.Condition = "item"; + + item1.ConditionChainsAreEquivalent(item2).Should().BeTrue(); + } + + [Fact] + public void ConditionChainsAreEquivalent_is_true_when_element_condition_matches_condition_of_other_element_parent() + { + var project = ProjectRootElement.Create(); + var itemGroup1 = project.AddItemGroup(); + var itemGroup2 = project.AddItemGroup(); + itemGroup1.Condition = "item"; + + var item1 = itemGroup1.AddItem("test", "include1"); + var item2 = itemGroup2.AddItem("test", "include2"); + item2.Condition = "item"; + + item1.ConditionChainsAreEquivalent(item2).Should().BeTrue(); + } + + [Fact] + public void ConditionChainsAreEquivalent_is_false_when_elements_have_different_conditions() + { + var project = ProjectRootElement.Create(); + var itemGroup = project.AddItemGroup(); + + var item1 = itemGroup.AddItem("test", "include1"); + var item2 = itemGroup.AddItem("test", "include2"); + item1.Condition = "item"; + item2.Condition = "item2"; + + item1.ConditionChainsAreEquivalent(item2).Should().BeFalse(); + } + + [Fact] + public void ConditionChainsAreEquivalent_is_false_when_other_element_parent_has_a_condition() + { + var project = ProjectRootElement.Create(); + var itemGroup1 = project.AddItemGroup(); + var itemGroup2 = project.AddItemGroup(); + itemGroup1.Condition = "item"; + + var item1 = itemGroup1.AddItem("test", "include1"); + var item2 = itemGroup2.AddItem("test", "include2"); + + item1.ConditionChainsAreEquivalent(item2).Should().BeFalse(); + } + + [Fact] + public void ConditionChainsAreEquivalent_is_false_when_both_element_parent_conditions_dont_match() + { + var project = ProjectRootElement.Create(); + var itemGroup1 = project.AddItemGroup(); + var itemGroup2 = project.AddItemGroup(); + itemGroup1.Condition = "item"; + itemGroup2.Condition = "item2"; + + var item1 = itemGroup1.AddItem("test", "include1"); + var item2 = itemGroup2.AddItem("test", "include2"); + + item1.ConditionChainsAreEquivalent(item2).Should().BeFalse(); + } + + [Fact] + public void HasConflictingMetadata_returns_true_when_items_have_metadata_with_same_name_but_different_value() + { + var project = ProjectRootElement.Create(); + var item1 = project.AddItem("test", "include1"); + item1.AddMetadata("name", "value"); + + var item2 = project.AddItem("test1", "include1"); + item2.AddMetadata("name", "value2"); + + item1.HasConflictingMetadata(item2).Should().BeTrue(); + } + + [Fact] + public void HasConflictingMetadata_returns_false_when_items_have_metadata_with_same_nameand_value() + { + var project = ProjectRootElement.Create(); + var item1 = project.AddItem("test", "include1"); + item1.AddMetadata("name", "value"); + + var item2 = project.AddItem("test1", "include1"); + item2.AddMetadata("name", "value"); + + item1.HasConflictingMetadata(item2).Should().BeFalse(); + } + [Fact] public void Includes_returns_include_value_split_by_semicolon() { diff --git a/test/Microsoft.DotNet.ProjectJsonMigration.Tests/Microsoft.DotNet.ProjectJsonMigration.Tests.xproj b/test/Microsoft.DotNet.ProjectJsonMigration.Tests/Microsoft.DotNet.ProjectJsonMigration.Tests.xproj index c4813d88e..4e2c370f6 100644 --- a/test/Microsoft.DotNet.ProjectJsonMigration.Tests/Microsoft.DotNet.ProjectJsonMigration.Tests.xproj +++ b/test/Microsoft.DotNet.ProjectJsonMigration.Tests/Microsoft.DotNet.ProjectJsonMigration.Tests.xproj @@ -8,8 +8,8 @@ 1F2EF070-AC5F-4078-AFB0-65745AC691B9 Microsoft.DotNet.ProjectJsonMigration.Tests - ..\artifacts\bin\ + ..\artifact\obj\$(RootNamespace) + ..\artifacts\bin\ 2.0 diff --git a/test/Microsoft.DotNet.ProjectJsonMigration.Tests/Rules/GivenThatIWantToMigrateBuildOptions.cs b/test/Microsoft.DotNet.ProjectJsonMigration.Tests/Rules/GivenThatIWantToMigrateBuildOptions.cs index 899a78204..8eb2d353f 100644 --- a/test/Microsoft.DotNet.ProjectJsonMigration.Tests/Rules/GivenThatIWantToMigrateBuildOptions.cs +++ b/test/Microsoft.DotNet.ProjectJsonMigration.Tests/Rules/GivenThatIWantToMigrateBuildOptions.cs @@ -48,18 +48,15 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Tests } [Fact] - public void Migrating_empty_buildOptions_populates_only_AssemblyName_and_OutputType() + public void Migrating_empty_buildOptions_populates_only_AssemblyName_Compile_and_EmbeddedResource() { var mockProj = RunBuildOptionsRuleOnPj(@" { ""buildOptions"": { } }"); - mockProj.Properties.Count().Should().Be(2); - mockProj.Properties.Any( - p => - !(p.Name.Equals("AssemblyName", StringComparison.Ordinal) || - p.Name.Equals("OutputType", StringComparison.Ordinal))).Should().BeFalse(); + mockProj.Properties.Count().Should().Be(1); + mockProj.Properties.Any(p => !p.Name.Equals("AssemblyName", StringComparison.Ordinal)).Should().BeFalse(); mockProj.Items.Count().Should().Be(2); mockProj.Items.First(i => i.ItemType == "Compile").Include.Should().Be(@"**\*.cs"); @@ -107,7 +104,8 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Tests }"); mockProj.Properties.Count(p => p.Name == "DefineConstants").Should().Be(1); - mockProj.Properties.First(p => p.Name == "DefineConstants").Value.Should().Be("DEBUG;TRACE"); + mockProj.Properties.First(p => p.Name == "DefineConstants") + .Value.Should().Be("$(DefineConstants);DEBUG;TRACE"); } [Fact] @@ -121,7 +119,7 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Tests }"); mockProj.Properties.Count(p => p.Name == "NoWarn").Should().Be(1); - mockProj.Properties.First(p => p.Name == "NoWarn").Value.Should().Be("CS0168;CS0219"); + mockProj.Properties.First(p => p.Name == "NoWarn").Value.Should().Be("$(NoWarn);CS0168;CS0219"); } [Fact] diff --git a/test/Microsoft.DotNet.ProjectJsonMigration.Tests/Rules/GivenThatIWantToMigrateConfigurations.cs b/test/Microsoft.DotNet.ProjectJsonMigration.Tests/Rules/GivenThatIWantToMigrateConfigurations.cs index a6348beaa..dce468606 100644 --- a/test/Microsoft.DotNet.ProjectJsonMigration.Tests/Rules/GivenThatIWantToMigrateConfigurations.cs +++ b/test/Microsoft.DotNet.ProjectJsonMigration.Tests/Rules/GivenThatIWantToMigrateConfigurations.cs @@ -39,6 +39,29 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Tests .Contain("'$(Configuration)' == 'testconfig'"); } + [Fact] + public void Frameworks_buildOptions_produce_expected_properties_in_a_group_with_a_condition() + { + var mockProj = RunConfigurationsRuleOnPj(@" + { + ""frameworks"": { + ""netcoreapp1.0"": { + ""buildOptions"": { + ""emitEntryPoint"": ""true"", + ""debugType"": ""full"" + } + } + } + }"); + + mockProj.Properties.Count( + prop => prop.Name == "OutputType" || prop.Name == "DebugType").Should().Be(2); + + mockProj.Properties.First(p => p.Name == "OutputType") + .Parent.Condition.Should() + .Contain("'$(TargetFrameworkIdentifier),Version=$(TargetFrameworkVersion)' == '.NETCoreApp,Version=v1.0'"); + } + [Fact] public void Configuration_buildOptions_properties_are_not_written_when_they_overlap_with_buildOptions() { @@ -67,11 +90,10 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Tests { property.Parent.Condition.Should().Be(string.Empty); } - } [Fact] - public void Configuration_buildOptions_includes_are_not_written_when_they_overlap_with_buildOptions() + public void Configuration_buildOptions_includes_and_Remove_are_written_when_they_differ_from_base_buildOptions() { var mockProj = RunConfigurationsAndBuildOptionsRuleOnPj(@" { @@ -97,25 +119,31 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Tests } }"); - mockProj.Items.Count(item => item.ItemType == "Content").Should().Be(3); + var contentItems = mockProj.Items.Where(item => item.ItemType == "Content"); - mockProj.Items.Where(item => item.ItemType == "Content") - .Count(item => !string.IsNullOrEmpty(item.Parent.Condition)) - .Should() - .Be(1); + contentItems.Count().Should().Be(4); - var configContent = mockProj.Items - .Where(item => item.ItemType == "Content").First(item => !string.IsNullOrEmpty(item.Parent.Condition)); + // 2 for Base Build options + contentItems.Where(i => i.ConditionChain().Count() == 0).Should().HaveCount(2); + + // 2 for Configuration BuildOptions (1 Remove, 1 Include) + contentItems.Where(i => i.ConditionChain().Count() == 1).Should().HaveCount(2); + + var configIncludeContentItem = contentItems.First( + item => item.ConditionChain().Count() > 0 && !string.IsNullOrEmpty(item.Include)); + var configRemoveContentItem = contentItems.First( + item => item.ConditionChain().Count() > 0 && !string.IsNullOrEmpty(item.Remove)); // Directories are not converted to globs in the result because we did not write the directory - configContent.Include.Should().Be(@"root;rootfile.cs"); - configContent.Exclude.Should().Be(@"src;rootfile.cs;src\file2.cs"); + configRemoveContentItem.Remove.Should().Be(@"root;src;rootfile.cs"); + configIncludeContentItem.Include.Should().Be(@"root;src;rootfile.cs"); + configIncludeContentItem.Exclude.Should().Be(@"src;rootfile.cs;src\file2.cs"); } [Fact] - public void Configuration_buildOptions_includes_which_have_different_excludes_than_buildOptions_throws() + public void Configuration_buildOptions_which_have_different_excludes_than_buildOptions_overwrites() { - Action action = () => RunConfigurationsRuleOnPj(@" + var mockProj = RunConfigurationsAndBuildOptionsRuleOnPj(@" { ""buildOptions"": { ""copyToOutput"": { @@ -130,7 +158,7 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Tests ""buildOptions"": { ""copyToOutput"": { ""include"": [""root"", ""src"", ""rootfile.cs""], - ""exclude"": [""src"", ""rootfile.cs""], + ""exclude"": [""rootfile.cs"", ""someotherfile.cs""], ""includeFiles"": [""src/file1.cs"", ""src/file2.cs""], ""excludeFiles"": [""src/file2.cs""] } @@ -139,10 +167,307 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Tests } }"); - action.ShouldThrow() - .WithMessage( - "MIGRATE20012::Configuration Exclude: Unable to migrate projects with excluded files in configurations."); + var contentItems = mockProj.Items.Where(item => item.ItemType == "Content"); + + contentItems.Count().Should().Be(5); + + // 2 for Base Build options + contentItems.Where(i => i.ConditionChain().Count() == 0).Should().HaveCount(2); + + // 3 for Configuration BuildOptions (1 Remove, 2 Include) + contentItems.Where(i => i.ConditionChain().Count() == 1).Should().HaveCount(3); + + var configIncludeContentItem = contentItems.First( + item => item.ConditionChain().Count() > 0 + && item.Include.Contains("root")); + + var configIncludeContentItem2 = contentItems.First( + item => item.ConditionChain().Count() > 0 + && item.Include.Contains(@"src\file1.cs")); + + var configRemoveContentItem = contentItems.First( + item => item.ConditionChain().Count() > 0 && !string.IsNullOrEmpty(item.Remove)); + + // Directories are not converted to globs in the result because we did not write the directory + configRemoveContentItem.Removes() + .Should().BeEquivalentTo("root", "src", "rootfile.cs", @"src\file1.cs", @"src\file2.cs"); + + configIncludeContentItem.Includes().Should().BeEquivalentTo("root", "src", "rootfile.cs"); + configIncludeContentItem.Excludes() + .Should().BeEquivalentTo("rootfile.cs", "someotherfile.cs", @"src\file2.cs"); + + configIncludeContentItem2.Includes().Should().BeEquivalentTo(@"src\file1.cs", @"src\file2.cs"); + configIncludeContentItem2.Excludes().Should().BeEquivalentTo(@"src\file2.cs"); } + + [Fact] + public void Configuration_buildOptions_which_have_mappings_to_directory_add_link_metadata_with_item_metadata() + { + var mockProj = RunConfigurationsAndBuildOptionsRuleOnPj(@" + { + ""configurations"": { + ""testconfig"": { + ""buildOptions"": { + ""copyToOutput"": { + ""mappings"": { + ""/some/dir/"" : { + ""include"": [""src"", ""root""], + ""exclude"": [""src"", ""rootfile.cs""], + ""includeFiles"": [""src/file1.cs"", ""src/file2.cs""], + ""excludeFiles"": [""src/file2.cs""] + } + } + } + } + } + } + }"); + + var contentItems = mockProj.Items.Where(item => item.ItemType == "Content"); + + contentItems.Count().Should().Be(2); + contentItems.Where(i => i.ConditionChain().Count() == 1).Should().HaveCount(2); + + var configIncludeContentItem = contentItems.First( + item => item.ConditionChain().Count() > 0 + && item.Include.Contains("root")); + + var configIncludeContentItem2 = contentItems.First( + item => item.ConditionChain().Count() > 0 + && item.Include.Contains(@"src\file1.cs")); + + configIncludeContentItem.Includes().Should().BeEquivalentTo("root", "src"); + configIncludeContentItem.Excludes() + .Should().BeEquivalentTo("rootfile.cs", "src", @"src\file2.cs"); + + configIncludeContentItem.GetMetadataWithName("Link").Should().NotBeNull(); + configIncludeContentItem.GetMetadataWithName("Link").Value.Should().Be("/some/dir/%(FileName)%(Extension)"); + + configIncludeContentItem.GetMetadataWithName("CopyToOutputDirectory").Should().NotBeNull(); + configIncludeContentItem.GetMetadataWithName("CopyToOutputDirectory").Value.Should().Be("PreserveNewest"); + + configIncludeContentItem2.Includes().Should().BeEquivalentTo(@"src\file1.cs", @"src\file2.cs"); + configIncludeContentItem2.Excludes().Should().BeEquivalentTo(@"src\file2.cs"); + + configIncludeContentItem2.GetMetadataWithName("Link").Should().NotBeNull(); + configIncludeContentItem2.GetMetadataWithName("Link").Value.Should().Be("/some/dir/%(FileName)%(Extension)"); + + configIncludeContentItem2.GetMetadataWithName("CopyToOutputDirectory").Should().NotBeNull(); + configIncludeContentItem2.GetMetadataWithName("CopyToOutputDirectory").Value.Should().Be("PreserveNewest"); + } + + [Fact] + public void Configuration_buildOptions_which_have_mappings_overlapping_with_includes_in_same_configuration_merged_items_have_Link_metadata() + { + var mockProj = RunConfigurationsAndBuildOptionsRuleOnPj(@" + { + ""configurations"": { + ""testconfig"": { + ""buildOptions"": { + ""copyToOutput"": { + ""include"": [""src"", ""root""], + ""exclude"": [""src"", ""rootfile.cs""], + ""includeFiles"": [""src/file1.cs""], + ""excludeFiles"": [""src/file2.cs""], + ""mappings"": { + ""/some/dir/"" : { + ""include"": [""src""], + ""exclude"": [""src"", ""src/rootfile.cs""] + } + } + } + } + } + } + }"); + + var contentItems = mockProj.Items.Where(item => item.ItemType == "Content"); + + contentItems.Count().Should().Be(3); + contentItems.Where(i => i.ConditionChain().Count() == 1).Should().HaveCount(3); + + var configIncludeContentItem = contentItems.First( + item => item.ConditionChain().Count() > 0 + && item.Include == "root"); + + var configIncludeContentItem2 = contentItems.First( + item => item.ConditionChain().Count() > 0 + && item.Include == "src"); + + var configIncludeContentItem3 = contentItems.First( + item => item.ConditionChain().Count() > 0 + && item.Include.Contains(@"src\file1.cs")); + + // Directories are not converted to globs in the result because we did not write the directory + + configIncludeContentItem.Includes().Should().BeEquivalentTo("root"); + configIncludeContentItem.Excludes() + .Should().BeEquivalentTo("rootfile.cs", "src", @"src\file2.cs"); + + configIncludeContentItem.GetMetadataWithName("Link").Should().BeNull(); + configIncludeContentItem.GetMetadataWithName("CopyToOutputDirectory").Should().NotBeNull(); + configIncludeContentItem.GetMetadataWithName("CopyToOutputDirectory").Value.Should().Be("PreserveNewest"); + + configIncludeContentItem2.Include.Should().Be("src"); + configIncludeContentItem2.Excludes().Should().BeEquivalentTo("src", "rootfile.cs", @"src\rootfile.cs", @"src\file2.cs"); + + configIncludeContentItem2.GetMetadataWithName("Link").Should().NotBeNull(); + configIncludeContentItem2.GetMetadataWithName("Link").Value.Should().Be("/some/dir/%(FileName)%(Extension)"); + configIncludeContentItem2.GetMetadataWithName("CopyToOutputDirectory").Should().NotBeNull(); + configIncludeContentItem2.GetMetadataWithName("CopyToOutputDirectory").Value.Should().Be("PreserveNewest"); + + configIncludeContentItem3.Includes().Should().BeEquivalentTo(@"src\file1.cs"); + configIncludeContentItem3.Exclude.Should().Be(@"src\file2.cs"); + + configIncludeContentItem3.GetMetadataWithName("Link").Should().BeNull(); + configIncludeContentItem3.GetMetadataWithName("CopyToOutputDirectory").Should().NotBeNull(); + configIncludeContentItem3.GetMetadataWithName("CopyToOutputDirectory").Value.Should().Be("PreserveNewest"); + } + + [Fact] + public void Configuration_buildOptions_which_have_mappings_overlapping_with_includes_in_root_buildoptions_has_remove() + { + var mockProj = RunConfigurationsAndBuildOptionsRuleOnPj(@" + { + ""buildOptions"" : { + ""copyToOutput"": { + ""include"": [""src"", ""root""], + ""exclude"": [""src"", ""rootfile.cs""], + ""includeFiles"": [""src/file1.cs"", ""src/file2.cs""], + ""excludeFiles"": [""src/file2.cs""] + } + }, + ""configurations"": { + ""testconfig"": { + ""buildOptions"": { + ""copyToOutput"": { + ""mappings"": { + ""/some/dir/"" : { + ""include"": [""src""], + ""exclude"": [""src"", ""rootfile.cs""] + } + } + } + } + } + } + }"); + + var contentItems = mockProj.Items.Where(item => item.ItemType == "Content"); + + contentItems.Count().Should().Be(4); + + var rootBuildOptionsContentItems = contentItems.Where(i => i.ConditionChain().Count() == 0).ToList(); + rootBuildOptionsContentItems.Count().Should().Be(2); + foreach (var buildOptionContentItem in rootBuildOptionsContentItems) + { + buildOptionContentItem.GetMetadataWithName("Link").Should().BeNull(); + buildOptionContentItem.GetMetadataWithName("CopyToOutputDirectory").Value.Should().Be("PreserveNewest"); + } + + var configItems = contentItems.Where(i => i.ConditionChain().Count() == 1); + configItems.Should().HaveCount(2); + + var configIncludeContentItem = contentItems.First( + item => item.ConditionChain().Count() > 0 + && item.Include.Contains("src")); + + var configRemoveContentItem = contentItems.First( + item => item.ConditionChain().Count() > 0 + && !string.IsNullOrEmpty(item.Remove)); + + configIncludeContentItem.Include.Should().Be("src"); + + configIncludeContentItem.GetMetadataWithName("Link").Should().NotBeNull(); + configIncludeContentItem.GetMetadataWithName("Link").Value.Should().Be("/some/dir/%(FileName)%(Extension)"); + + configIncludeContentItem.GetMetadataWithName("CopyToOutputDirectory").Should().NotBeNull(); + configIncludeContentItem.GetMetadataWithName("CopyToOutputDirectory").Value.Should().Be("PreserveNewest"); + + configRemoveContentItem.Remove.Should().Be("src"); + } + + [Fact] + public void Configuration_buildOptions_which_have_mappings_overlapping_with_includes_in_same_configuration_and_root_buildOptions_have_removes_and_Link_metadata_and_encompassed_items_are_merged() + { + var mockProj = RunConfigurationsAndBuildOptionsRuleOnPj(@" + { + ""buildOptions"" : { + ""copyToOutput"": { + ""include"": [""src"", ""root""], + ""exclude"": [""src"", ""rootfile.cs""], + ""includeFiles"": [""src/file1.cs""], + ""excludeFiles"": [""src/file2.cs""] + } + }, + ""configurations"": { + ""testconfig"": { + ""buildOptions"": { + ""copyToOutput"": { + ""include"": [""src"", ""root""], + ""exclude"": [""src"", ""rootfile.cs""], + ""includeFiles"": [""src/file3.cs""], + ""excludeFiles"": [""src/file2.cs""], + ""mappings"": { + ""/some/dir/"" : { + ""include"": [""src""], + ""exclude"": [""src"", ""src/rootfile.cs""] + } + } + } + } + } + } + }"); + + var contentItems = mockProj.Items.Where(item => item.ItemType == "Content"); + + contentItems.Count().Should().Be(5); + contentItems.Where(i => i.ConditionChain().Count() == 1).Should().HaveCount(3); + + var rootBuildOptionsContentItems = contentItems.Where(i => i.ConditionChain().Count() == 0).ToList(); + rootBuildOptionsContentItems.Count().Should().Be(2); + foreach (var buildOptionContentItem in rootBuildOptionsContentItems) + { + buildOptionContentItem.GetMetadataWithName("Link").Should().BeNull(); + buildOptionContentItem.GetMetadataWithName("CopyToOutputDirectory").Value.Should().Be("PreserveNewest"); + } + + var configIncludeEncompassedItem = contentItems.FirstOrDefault( + item => item.ConditionChain().Count() > 0 + && item.Include == "root"); + configIncludeEncompassedItem.Should().BeNull(); + + var configIncludeContentItem = contentItems.First( + item => item.ConditionChain().Count() > 0 + && item.Include == "src"); + + var configIncludeContentItem2 = contentItems.First( + item => item.ConditionChain().Count() > 0 + && item.Include.Contains(@"src\file3.cs")); + + var configRemoveContentItem = contentItems.First( + item => item.ConditionChain().Count() > 0 + && !string.IsNullOrEmpty(item.Remove)); + + configIncludeContentItem.Include.Should().Be("src"); + configIncludeContentItem.Excludes().Should().BeEquivalentTo("src", "rootfile.cs", @"src\rootfile.cs", @"src\file2.cs"); + + configIncludeContentItem.GetMetadataWithName("Link").Should().NotBeNull(); + configIncludeContentItem.GetMetadataWithName("Link").Value.Should().Be("/some/dir/%(FileName)%(Extension)"); + configIncludeContentItem.GetMetadataWithName("CopyToOutputDirectory").Should().NotBeNull(); + configIncludeContentItem.GetMetadataWithName("CopyToOutputDirectory").Value.Should().Be("PreserveNewest"); + + configIncludeContentItem2.Includes().Should().BeEquivalentTo(@"src\file3.cs"); + configIncludeContentItem2.Exclude.Should().Be(@"src\file2.cs"); + + configIncludeContentItem2.GetMetadataWithName("Link").Should().BeNull(); + configIncludeContentItem2.GetMetadataWithName("CopyToOutputDirectory").Should().NotBeNull(); + configIncludeContentItem2.GetMetadataWithName("CopyToOutputDirectory").Value.Should().Be("PreserveNewest"); + + configRemoveContentItem.Removes().Should().BeEquivalentTo("src"); + } + private ProjectRootElement RunConfigurationsRuleOnPj(string s, string testDirectory = null) { testDirectory = testDirectory ?? Temp.CreateDirectory().Path; diff --git a/test/dotnet-migrate.Tests/GivenThatIWantToMigrateTestApps.cs b/test/dotnet-migrate.Tests/GivenThatIWantToMigrateTestApps.cs index 5da4741c7..645e4cb0f 100644 --- a/test/dotnet-migrate.Tests/GivenThatIWantToMigrateTestApps.cs +++ b/test/dotnet-migrate.Tests/GivenThatIWantToMigrateTestApps.cs @@ -25,6 +25,7 @@ namespace Microsoft.DotNet.Migration.Tests // TODO: Standalone apps [InlineData("TestAppSimple", false)] // https://github.com/dotnet/sdk/issues/73 [InlineData("TestAppWithLibrary/TestApp", false)] [InlineData("TestAppWithRuntimeOptions")] + [InlineData("TestAppWithContents")] public void It_migrates_apps(string projectName) { var projectDirectory = TestAssetsManager.CreateTestInstance(projectName, callingMethod: "i").WithLockFiles().Path; @@ -204,6 +205,8 @@ namespace Microsoft.DotNet.Migration.Tests private string BuildMSBuild(string projectDirectory, string configuration="Debug") { + DeleteXproj(projectDirectory); + var result = new Build3Command() .WithWorkingDirectory(projectDirectory) .ExecuteWithCapturedOutput($"/p:Configuration={configuration}"); @@ -215,6 +218,15 @@ namespace Microsoft.DotNet.Migration.Tests return result.StdOut; } + private void DeleteXproj(string projectDirectory) + { + var xprojFiles = Directory.EnumerateFiles(projectDirectory, "*.xproj"); + foreach (var xprojFile in xprojFiles) + { + File.Delete(xprojFile); + } + } + private void OutputDiagnostics(MigratedBuildComparisonData comparisonData) { OutputDiagnostics(comparisonData.MSBuildBuildOutputs, comparisonData.ProjectJsonBuildOutputs);