|
/* |
|
* Copyright (C) 2019 Greg Walker <https://github.com/eibwen> |
|
* |
|
* This program is free software: you can redistribute it and/or modify |
|
* it under the terms of the GNU General Public License as published by |
|
* the Free Software Foundation; either version 3 of the License, or |
|
* (at your option) any later version. |
|
* |
|
* This program is distributed in the hope that it will be useful, |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
* GNU General Public License for more details. |
|
* |
|
* Shared under GNU GPLv3, see <https://www.gnu.org/licenses/>. |
|
*/ |
|
|
|
using System; |
|
using System.Collections.Generic; |
|
using System.IO; |
|
using System.Linq; |
|
using System.Xml.Linq; |
|
|
|
namespace Common.Solution.UnitTests |
|
{ |
|
/// <summary> |
|
/// Helper methods for finding and parsing .csproj files |
|
/// </summary> |
|
public class ProjectFileHelpers |
|
{ |
|
/// <summary> |
|
/// Find all references that a project file contains |
|
/// </summary> |
|
/// <remarks>This always tries to parse ALL references regardless of the <see cref="filterTypes"/>, and filters afterward</remarks> |
|
internal static Dictionary<string, List<Reference>> GetAllProjectReferences(ReferenceType filterTypes = ReferenceType.All) |
|
{ |
|
var baseDirectory = FileTestHelpers.GetBaseDirectory(); |
|
var projects = FileTestHelpers.GetAllProjectFiles(); |
|
|
|
var references = projects.Select(x => new |
|
{ |
|
Project = x, |
|
References = Reference.ParseProjFile(baseDirectory, x.File).Where(f => filterTypes.HasFlag(f.Type)) |
|
}); |
|
|
|
return references.ToDictionary(x => x.Project.File, x => x.References.ToList()); |
|
} |
|
|
|
//TODO should maybe make a XElement version of this?: |
|
/// <summary> |
|
/// Allows you to find arbitrary lines from the project file, somewhat limited usage |
|
/// </summary> |
|
/// <remarks>The lines are trimmed before being passed the the <see cref="matcher"/>, so do not include whitespace</remarks> |
|
internal static Dictionary<string, List<string>> GetMatchingProjectFileLines(Func<string, bool> matcher) |
|
{ |
|
var baseDirectory = FileTestHelpers.GetBaseDirectory(); |
|
var projects = FileTestHelpers.GetAllProjectFiles(); |
|
|
|
var references = projects.Select(x => new |
|
{ |
|
Project = x, |
|
Lines = File.ReadLines(Path.Combine(baseDirectory, x.File)).Select(l => l.Trim()).Where(matcher) |
|
}); |
|
|
|
return references.ToDictionary(x => x.Project.File, x => x.Lines.ToList()); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// A class to parse and store all types of references a project might contain |
|
/// </summary> |
|
internal class Reference |
|
{ |
|
public static IEnumerable<Reference> ParseProjFile(string baseDirectory, string projectFile) |
|
{ |
|
var xele = XElement.Load(Path.Combine(baseDirectory, projectFile)); |
|
xele = StripNamespaces(xele); |
|
var itemGroupChildren = xele.Elements("ItemGroup").SelectMany(x => x.Elements()); |
|
|
|
return itemGroupChildren.Select(x => ParseElement(projectFile, x)) |
|
.Where(x => x != null); |
|
} |
|
public static IEnumerable<Reference> ParseProjContents(string projectFilepath, string fileContent) |
|
{ |
|
var projectFile = Path.GetFileName(projectFilepath); |
|
|
|
var xele = XElement.Parse(fileContent); |
|
xele = StripNamespaces(xele); |
|
var itemGroupChildren = xele.Elements("ItemGroup").SelectMany(x => x.Elements()); |
|
|
|
return itemGroupChildren.Select(x => ParseElement(projectFile, x)) |
|
.Where(x => x != null); |
|
} |
|
|
|
// Source: https://stackoverflow.com/a/1147012/356218 |
|
private static XElement StripNamespaces(XElement rootElement) |
|
{ |
|
foreach (var element in rootElement.DescendantsAndSelf()) |
|
{ |
|
// update element name if a namespace is available |
|
if (element.Name.Namespace != XNamespace.None) |
|
{ |
|
element.Name = XNamespace.None.GetName(element.Name.LocalName); |
|
} |
|
|
|
// check if the element contains attributes with defined namespaces (ignore xml and empty namespaces) |
|
bool hasDefinedNamespaces = element.Attributes().Any(attribute => attribute.IsNamespaceDeclaration || |
|
(attribute.Name.Namespace != XNamespace.None && attribute.Name.Namespace != XNamespace.Xml)); |
|
|
|
if (hasDefinedNamespaces) |
|
{ |
|
// ignore attributes with a namespace declaration |
|
// strip namespace from attributes with defined namespaces, ignore xml / empty namespaces |
|
// xml namespace is ignored to retain the space preserve attribute |
|
var attributes = element.Attributes() |
|
.Where(attribute => !attribute.IsNamespaceDeclaration) |
|
.Select(attribute => |
|
(attribute.Name.Namespace != XNamespace.None && attribute.Name.Namespace != XNamespace.Xml) ? |
|
new XAttribute(XNamespace.None.GetName(attribute.Name.LocalName), attribute.Value) : |
|
attribute |
|
); |
|
|
|
// replace with attributes result |
|
element.ReplaceAttributes(attributes); |
|
} |
|
} |
|
return rootElement; |
|
} |
|
|
|
private static Reference ParseElement(string project, XElement reference) |
|
{ |
|
switch (reference.Name.LocalName) |
|
{ |
|
case "PackageReference": |
|
if (reference.Attribute("Update") != null) |
|
{ |
|
reference.Dump("find me documentation for this please, wtf is it"); |
|
return null; |
|
} |
|
return new Reference(project, |
|
ReferenceType.NugetPackage, |
|
reference.Attribute("Include").Value, |
|
reference.Attribute("Version")?.Value); |
|
|
|
case "ProjectReference": |
|
return new Reference(project, |
|
ReferenceType.Project, |
|
reference.Attribute("Include").Value, |
|
null); |
|
|
|
case "Reference": |
|
if (reference.Attributes().Count() > 1) |
|
{ |
|
reference.Dump(); |
|
reference.Attributes().Dump(); |
|
throw new ArgumentException($"<Reference> has more attributes than expected in {project}"); |
|
} |
|
var allowedElements = new HashSet<string> |
|
{ |
|
"HintPath", |
|
"SpecificVersion", |
|
"Private", |
|
"RequiredTargetFramework", |
|
"EmbedInteropTypes", // super special case |
|
}; |
|
if (reference.Elements().Count(x => !allowedElements.Contains(x.Name.LocalName)) > 0) |
|
{ |
|
reference.Dump(); |
|
reference.Elements().Dump(); |
|
throw new ArgumentException($"<Reference> has more children than expected in {project}"); |
|
} |
|
return new Reference(project, |
|
ReferenceType.DllReference, |
|
reference.Attribute("Include").Value, |
|
null, |
|
reference.Element("HintPath")?.Value); |
|
|
|
case "AdditionalFiles": |
|
return new Reference(project, |
|
ReferenceType.AdditionalFile, |
|
reference.Attribute("Include").Value, |
|
null); |
|
|
|
case "Resource": |
|
return new Reference(project, |
|
ReferenceType.Resource, |
|
reference.Attribute("Include").Value, |
|
null); |
|
|
|
case "Analyzer": |
|
return new Reference(project, |
|
ReferenceType.Analyzer, |
|
reference.Attribute("Include").Value, |
|
null); |
|
|
|
case "EntityDeploy": |
|
return new Reference(project, |
|
ReferenceType.EntityDataModel, |
|
reference.Attribute("Include").Value, |
|
null); |
|
|
|
case "Content": |
|
case "ContentWithTargetPath": |
|
case "Folder": |
|
case "Compile": |
|
case "EmbeddedResource": |
|
case "None": |
|
case "Service": // Just ignoring Service cause I'm not using it for anything |
|
case "AppSettingsFiles": // Again not useful to me |
|
case "AssemblyAttribute": |
|
case "WCFMetadata": // Even more gross |
|
case "WCFMetadataStorage": |
|
case "RuntimeHostConfigurationOption": // Not useful |
|
case "DotNetCliToolReference": |
|
case "BootstrapperPackage": |
|
return null; |
|
|
|
case "Page": // UI shit? |
|
case "ApplicationDefinition": |
|
case "AppDesigner": |
|
return null; |
|
|
|
// Extension properties |
|
case "SpecFlowFeatureFiles": // Gross |
|
case "NukeMetadata": |
|
case "Protobuf": |
|
return null; |
|
|
|
case "SpecFlowObsoleteCodeBehindFiles": |
|
$"## This should be cleaned up?".Dump(); |
|
return null; |
|
|
|
default: |
|
throw new NotSupportedException($"Do not know how to parse element {reference.Name}, which was found in {project}"); |
|
} |
|
} |
|
|
|
public Reference(string project, ReferenceType type, string name, string version, string hintPath = null) |
|
{ |
|
Project = project; |
|
Name = name; |
|
Version = version; |
|
HintPath = hintPath; |
|
Type = type; |
|
} |
|
|
|
public string Project { get; } |
|
public ReferenceType Type { get; } |
|
|
|
public string Name { get; } |
|
public string Version { get; } |
|
public string HintPath { get; } |
|
} |
|
|
|
[Flags] |
|
internal enum ReferenceType |
|
{ |
|
Default = 0, |
|
NugetPackage = (1 << 0), |
|
Project = (1 << 1), |
|
DllReference = (1 << 2), |
|
AdditionalFile = (1 << 3), |
|
Resource = (1 << 4), |
|
Analyzer = (1 << 5), |
|
EntityDataModel = (1 << 6), |
|
|
|
All = Default |
|
| NugetPackage |
|
| Project |
|
| DllReference |
|
| AdditionalFile |
|
| Resource |
|
| Analyzer |
|
| EntityDataModel |
|
} |
|
} |