Skip to content

Commit

Permalink
Merge pull request #918 from DustinCampbell/new-solution-parser
Browse files Browse the repository at this point in the history
Fix solution parsing (again!)
  • Loading branch information
david-driscoll authored Jul 19, 2017
2 parents 4ed7ff3 + 6604c56 commit 91b884f
Show file tree
Hide file tree
Showing 10 changed files with 724 additions and 4 deletions.
8 changes: 4 additions & 4 deletions src/OmniSharp.MSBuild/MSBuildProjectSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Build.Construction;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Extensions.Configuration;
Expand All @@ -18,6 +17,7 @@
using OmniSharp.MSBuild.Models.Events;
using OmniSharp.MSBuild.ProjectFile;
using OmniSharp.MSBuild.Resolution;
using OmniSharp.MSBuild.SolutionParsing;
using OmniSharp.Options;
using OmniSharp.Services;

Expand Down Expand Up @@ -138,13 +138,13 @@ private IEnumerable<string> GetProjectPathsFromSolution(string solutionFilePath)
{
_logger.LogInformation($"Detecting projects in '{solutionFilePath}'.");

var solutionFile = SolutionFile.Parse(solutionFilePath);
var solutionFile = SolutionFile.ParseFile(solutionFilePath);
var processedProjects = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var result = new List<string>();

foreach (var project in solutionFile.ProjectsInOrder)
foreach (var project in solutionFile.Projects)
{
if (project.ProjectType == SolutionProjectType.SolutionFolder)
if (project.IsSolutionFolder)
{
continue;
}
Expand Down
20 changes: 20 additions & 0 deletions src/OmniSharp.MSBuild/SolutionParsing/GlobalSectionBlock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Immutable;

namespace OmniSharp.MSBuild.SolutionParsing
{
internal sealed class GlobalSectionBlock : SectionBlock
{
private GlobalSectionBlock(string name, ImmutableArray<Property> properties)
: base(name, properties)
{
}

public static GlobalSectionBlock Parse(string headerLine, Scanner scanner)
{
var (name, properties) = ParseNameAndProperties(
"GlobalSection", "EndGlobalSection", headerLine, scanner);

return new GlobalSectionBlock(name, properties);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;

namespace OmniSharp.MSBuild.SolutionParsing
{
internal class InvalidSolutionFileException : Exception
{
public InvalidSolutionFileException()
{
}

public InvalidSolutionFileException(string message) : base(message)
{
}

public InvalidSolutionFileException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
98 changes: 98 additions & 0 deletions src/OmniSharp.MSBuild/SolutionParsing/ProjectBlock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System;
using System.Collections.Immutable;
using System.IO;
using System.Text.RegularExpressions;

namespace OmniSharp.MSBuild.SolutionParsing
{
internal class ProjectBlock
{
private const string SolutionFolderGuid = "{2150E333-8FDC-42A3-9474-1A3956D46DE8}";

// An example of a project line looks like this:
// Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClassLibrary1", "ClassLibrary1\ClassLibrary1.csproj", "{DEBCE986-61B9-435E-8018-44B9EF751655}"
private static readonly Lazy<Regex> s_lazyProjectHeader = new Lazy<Regex>(
() => new Regex
(
"^" // Beginning of line
+ "Project\\(\"(?<PROJECTTYPEGUID>.*)\"\\)"
+ "\\s*=\\s*" // Any amount of whitespace plus "=" plus any amount of whitespace
+ "\"(?<PROJECTNAME>.*)\""
+ "\\s*,\\s*" // Any amount of whitespace plus "," plus any amount of whitespace
+ "\"(?<RELATIVEPATH>.*)\""
+ "\\s*,\\s*" // Any amount of whitespace plus "," plus any amount of whitespace
+ "\"(?<PROJECTGUID>.*)\""
+ "$", // End-of-line
RegexOptions.Compiled)
);

public string ProjectTypeGuid { get; }
public string ProjectName { get; }
public string RelativePath { get; }
public string ProjectGuid { get; }
public ImmutableArray<SectionBlock> Sections { get; }

public bool IsSolutionFolder => ProjectTypeGuid.Equals(SolutionFolderGuid, StringComparison.OrdinalIgnoreCase);

private ProjectBlock(string projectTypeGuid, string projectName, string relativePath, string projectGuid, ImmutableArray<SectionBlock> sections)
{
ProjectTypeGuid = projectTypeGuid;
ProjectName = projectName;
RelativePath = relativePath;
ProjectGuid = projectGuid;
Sections = sections;
}

public static ProjectBlock Parse(string headerLine, Scanner scanner)
{
var match = s_lazyProjectHeader.Value.Match(headerLine);
if (!match.Success)
{
return null;
}

var projectTypeGuid = match.Groups["PROJECTTYPEGUID"].Value.Trim();
var projectName = match.Groups["PROJECTNAME"].Value.Trim();
var relativePath = match.Groups["RELATIVEPATH"].Value.Trim();
var projectGuid = match.Groups["PROJECTGUID"].Value.Trim();

// If the project name is empty, set it to a generated generic value.
if (string.IsNullOrEmpty(projectName))
{
projectName = "EmptyProjectName." + Guid.NewGuid();
}

if (relativePath.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
{
throw new InvalidSolutionFileException("A project path contains an invalid character.");
}

var sections = ImmutableArray.CreateBuilder<SectionBlock>();

// Search for project dependencies. Keep reading until we either...
// 1. reach the end of the file,
// 2. see "ProjectSection( at the beginning of the line, or
// 3. see "EndProject at the beginning of the line.

string line;
while ((line = scanner.NextLine()) != null)
{
if (line == "EndProject")
{
break;
}

if (line.StartsWith("ProjectSection("))
{
var section = ProjectSectionBlock.Parse(line, scanner);
if (section != null)
{
sections.Add(section);
}
}
}

return new ProjectBlock(projectTypeGuid, projectName, relativePath, projectGuid, sections.ToImmutable());
}
}
}
20 changes: 20 additions & 0 deletions src/OmniSharp.MSBuild/SolutionParsing/ProjectSectionBlock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Immutable;

namespace OmniSharp.MSBuild.SolutionParsing
{
internal sealed class ProjectSectionBlock : SectionBlock
{
private ProjectSectionBlock(string name, ImmutableArray<Property> properties)
: base(name, properties)
{
}

public static ProjectSectionBlock Parse(string headerLine, Scanner scanner)
{
var (name, properties) = ParseNameAndProperties(
"ProjectSection", "EndProjectSection", headerLine, scanner);

return new ProjectSectionBlock(name, properties);
}
}
}
47 changes: 47 additions & 0 deletions src/OmniSharp.MSBuild/SolutionParsing/Property.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Text.RegularExpressions;

namespace OmniSharp.MSBuild.SolutionParsing
{
internal sealed class Property
{
// An example of a property line looks like this:
// AspNetCompiler.VirtualPath = "/webprecompile"
// Because website projects now include the target framework moniker as
// one of their properties, <PROPERTYVALUE> may have an '=' in it.
private static readonly Lazy<Regex> s_lazyPropertyLine = new Lazy<Regex>(
() => new Regex
(
"^" // Beginning of line
+ "(?<PROPERTYNAME>[^=]*)"
+ "\\s*=\\s*" // Any amount of whitespace plus "=" plus any amount of whitespace
+ "(?<PROPERTYVALUE>.*)"
+ "$", // End-of-line
RegexOptions.Compiled)
);

public string Name { get; }
public string Value { get; }

private Property(string name, string value)
{
Name = name;
Value = value;
}

public static Property Parse(string propertyLine)
{
var match = s_lazyPropertyLine.Value.Match(propertyLine);

if (!match.Success)
{
return null;
}

var name = match.Groups["PROPERTYNAME"].Value.Trim();
var value = match.Groups["PROPERTYVALUE"].Value.Trim();

return new Property(name, value);
}
}
}
36 changes: 36 additions & 0 deletions src/OmniSharp.MSBuild/SolutionParsing/Scanner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.IO;

namespace OmniSharp.MSBuild.SolutionParsing
{
internal class Scanner : IDisposable
{
private readonly StringReader _reader;
private int _currentLineNumber;

public Scanner(string text)
{
_reader = new StringReader(text);
}

public void Dispose()
{
_reader.Dispose();
}

public string NextLine()
{
var line = _reader.ReadLine();

_currentLineNumber++;

if (line != null)
{
line = line.Trim();
}

return line;
}
}
}
51 changes: 51 additions & 0 deletions src/OmniSharp.MSBuild/SolutionParsing/SectionBlock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Collections.Immutable;

namespace OmniSharp.MSBuild.SolutionParsing
{
internal abstract class SectionBlock
{
public string Name { get; }
public ImmutableArray<Property> Properties { get; }

protected SectionBlock(string name, ImmutableArray<Property> properties)
{
Name = name;
Properties = properties;
}

protected static (string name, ImmutableArray<Property> properties) ParseNameAndProperties(
string startSection, string endSection, string headerLine, Scanner scanner)
{
var startIndex = startSection.Length;
if (!startSection.EndsWith("("))
{
startIndex++;
}

var endIndex = headerLine.IndexOf(')', startIndex);
var name = endIndex >= startIndex
? headerLine.Substring(startIndex, endIndex - startIndex)
: headerLine.Substring(startIndex);

var properties = ImmutableArray.CreateBuilder<Property>();

string line;
while ((line = scanner.NextLine()) != null)
{
if (line.StartsWith(endSection, StringComparison.Ordinal))
{
break;
}

var property = Property.Parse(line);
if (property != null)
{
properties.Add(property);
}
}

return (name, properties.ToImmutable());
}
}
}
Loading

0 comments on commit 91b884f

Please sign in to comment.