CMake/Source/cmVisualStudioSlnParser.cxx
Petr Kmoch df035e4825 VS: Create parser for Visual Studio .sln files
Create class cmVisualStudioSlnParser as a generic parser for Visual
Studio .sln files. Implement minimum functionality but keep class
extensible.  Add tests for the class.
2013-04-12 11:35:35 -04:00

713 lines
23 KiB
C++

/*============================================================================
CMake - Cross Platform Makefile Generator
Copyright 2000-2013 Kitware, Inc., Insight Software Consortium
Distributed under the OSI-approved BSD License (the "License");
see accompanying file Copyright.txt for details.
This software is distributed WITHOUT ANY WARRANTY; without even the
implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the License for more information.
============================================================================*/
#include "cmVisualStudioSlnParser.h"
#include "cmSystemTools.h"
#include "cmVisualStudioSlnData.h"
#include <cassert>
#include <stack>
//----------------------------------------------------------------------------
namespace
{
enum LineFormat
{
LineMultiValueTag,
LineSingleValueTag,
LineKeyValuePair,
LineVerbatim
};
}
//----------------------------------------------------------------------------
class cmVisualStudioSlnParser::ParsedLine
{
public:
bool IsComment() const;
bool IsKeyValuePair() const;
const std::string& GetTag() const { return this->Tag; }
const std::string& GetArg() const { return this->Arg.first; }
std::string GetArgVerbatim() const;
size_t GetValueCount() const { return this->Values.size(); }
const std::string& GetValue(size_t idxValue) const;
std::string GetValueVerbatim(size_t idxValue) const;
void SetTag(const std::string& tag) { this->Tag = tag; }
void SetArg(const std::string& arg) { this->Arg = StringData(arg, false); }
void SetQuotedArg(const std::string& arg)
{ this->Arg = StringData(arg, true); }
void AddValue(const std::string& value)
{ this->Values.push_back(StringData(value, false)); }
void AddQuotedValue(const std::string& value)
{ this->Values.push_back(StringData(value, true)); }
void CopyVerbatim(const std::string& line) { this->Tag = line; }
private:
typedef std::pair<std::string, bool> StringData;
std::string Tag;
StringData Arg;
std::vector<StringData> Values;
static const std::string BadString;
static const std::string Quote;
};
//----------------------------------------------------------------------------
const std::string cmVisualStudioSlnParser::ParsedLine::BadString;
const std::string cmVisualStudioSlnParser::ParsedLine::Quote("\"");
//----------------------------------------------------------------------------
bool cmVisualStudioSlnParser::ParsedLine::IsComment() const
{
assert(!this->Tag.empty());
return (this->Tag[0]== '#');
}
//----------------------------------------------------------------------------
bool cmVisualStudioSlnParser::ParsedLine::IsKeyValuePair() const
{
assert(!this->Tag.empty());
return this->Arg.first.empty() && this->Values.size() == 1;
}
//----------------------------------------------------------------------------
std::string cmVisualStudioSlnParser::ParsedLine::GetArgVerbatim() const
{
if (this->Arg.second)
return Quote + this->Arg.first + Quote;
else
return this->Arg.first;
}
//----------------------------------------------------------------------------
const std::string&
cmVisualStudioSlnParser::ParsedLine::GetValue(size_t idxValue) const
{
if (idxValue < this->Values.size())
return this->Values[idxValue].first;
else
return BadString;
}
//----------------------------------------------------------------------------
std::string
cmVisualStudioSlnParser::ParsedLine::GetValueVerbatim(size_t idxValue) const
{
if (idxValue < this->Values.size())
{
const StringData& data = this->Values[idxValue];
if (data.second)
return Quote + data.first + Quote;
else
return data.first;
}
else
return BadString;
}
//----------------------------------------------------------------------------
class cmVisualStudioSlnParser::State
{
public:
explicit State(DataGroupSet requestedData);
size_t GetCurrentLine() const { return this->CurrentLine; }
bool ReadLine(std::istream& input, std::string& line);
LineFormat NextLineFormat() const;
bool Process(const cmVisualStudioSlnParser::ParsedLine& line,
cmSlnData& output,
cmVisualStudioSlnParser::ResultData& result);
bool Finished(cmVisualStudioSlnParser::ResultData& result);
private:
enum FileState
{
FileStateStart,
FileStateTopLevel,
FileStateProject,
FileStateProjectDependencies,
FileStateGlobal,
FileStateSolutionConfigurations,
FileStateProjectConfigurations,
FileStateSolutionFilters,
FileStateGlobalSection,
FileStateIgnore
};
std::stack<FileState> Stack;
std::string EndIgnoreTag;
DataGroupSet RequestedData;
size_t CurrentLine;
void IgnoreUntilTag(const std::string& endTag);
};
//----------------------------------------------------------------------------
cmVisualStudioSlnParser::State::State(DataGroupSet requestedData) :
RequestedData(requestedData),
CurrentLine(0)
{
if (this->RequestedData.test(DataGroupProjectDependenciesBit))
this->RequestedData.set(DataGroupProjectsBit);
this->Stack.push(FileStateStart);
}
//----------------------------------------------------------------------------
bool cmVisualStudioSlnParser::State::ReadLine(std::istream& input,
std::string& line)
{
++this->CurrentLine;
return !std::getline(input, line).fail();
}
//----------------------------------------------------------------------------
LineFormat cmVisualStudioSlnParser::State::NextLineFormat() const
{
switch (this->Stack.top())
{
case FileStateStart: return LineVerbatim;
case FileStateTopLevel: return LineMultiValueTag;
case FileStateProject: return LineSingleValueTag;
case FileStateProjectDependencies: return LineKeyValuePair;
case FileStateGlobal: return LineSingleValueTag;
case FileStateSolutionConfigurations: return LineKeyValuePair;
case FileStateProjectConfigurations: return LineKeyValuePair;
case FileStateSolutionFilters: return LineKeyValuePair;
case FileStateGlobalSection: return LineKeyValuePair;
case FileStateIgnore: return LineVerbatim;
default:
assert(false);
return LineVerbatim;
}
}
//----------------------------------------------------------------------------
bool cmVisualStudioSlnParser::State::Process(
const cmVisualStudioSlnParser::ParsedLine& line,
cmSlnData& output, cmVisualStudioSlnParser::ResultData& result)
{
assert(!line.IsComment());
switch (this->Stack.top())
{
case FileStateStart:
if (!cmSystemTools::StringStartsWith(
line.GetTag().c_str(), "Microsoft Visual Studio Solution File"))
{
result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
return false;
}
this->Stack.pop();
this->Stack.push(FileStateTopLevel);
break;
case FileStateTopLevel:
if (line.GetTag().compare("Project") == 0)
{
if (line.GetValueCount() != 3)
{
result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
return false;
}
if (this->RequestedData.test(DataGroupProjectsBit))
{
if (!output.AddProject(line.GetValue(2),
line.GetValue(0),
line.GetValue(1)))
{
result.SetError(ResultErrorInputData, this->GetCurrentLine());
return false;
}
this->Stack.push(FileStateProject);
}
else
this->IgnoreUntilTag("EndProject");
}
else if (line.GetTag().compare("Global") == 0)
this->Stack.push(FileStateGlobal);
else
{
result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
return false;
}
break;
case FileStateProject:
if (line.GetTag().compare("EndProject") == 0)
this->Stack.pop();
else if (line.GetTag().compare("ProjectSection") == 0)
{
if (line.GetArg().compare("ProjectDependencies") == 0 &&
line.GetValue(0).compare("postProject") == 0)
{
if (this->RequestedData.test(DataGroupProjectDependenciesBit))
this->Stack.push(FileStateProjectDependencies);
else
this->IgnoreUntilTag("EndProjectSection");
}
else
this->IgnoreUntilTag("EndProjectSection");
}
else
{
result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
return false;
}
break;
case FileStateProjectDependencies:
if (line.GetTag().compare("EndProjectSection") == 0)
this->Stack.pop();
else if (line.IsKeyValuePair())
// implement dependency storing here, once needed
;
else
{
result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
return false;
}
break;
case FileStateGlobal:
if (line.GetTag().compare("EndGlobal") == 0)
this->Stack.pop();
else if (line.GetTag().compare("GlobalSection") == 0)
{
if (line.GetArg().compare("SolutionConfigurationPlatforms") == 0 &&
line.GetValue(0).compare("preSolution") == 0)
{
if (this->RequestedData.test(DataGroupSolutionConfigurationsBit))
this->Stack.push(FileStateSolutionConfigurations);
else
this->IgnoreUntilTag("EndGlobalSection");
}
else if (line.GetArg().compare("ProjectConfigurationPlatforms") == 0 &&
line.GetValue(0).compare("postSolution") == 0)
{
if (this->RequestedData.test(DataGroupProjectConfigurationsBit))
this->Stack.push(FileStateProjectConfigurations);
else
this->IgnoreUntilTag("EndGlobalSection");
}
else if (line.GetArg().compare("NestedProjects") == 0 &&
line.GetValue(0).compare("preSolution") == 0)
{
if (this->RequestedData.test(DataGroupSolutionFiltersBit))
this->Stack.push(FileStateSolutionFilters);
else
this->IgnoreUntilTag("EndGlobalSection");
}
else if (this->RequestedData.test(DataGroupGenericGlobalSectionsBit))
this->Stack.push(FileStateGlobalSection);
else
this->IgnoreUntilTag("EndGlobalSection");
}
else
{
result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
return false;
}
break;
case FileStateSolutionConfigurations:
if (line.GetTag().compare("EndGlobalSection") == 0)
this->Stack.pop();
else if (line.IsKeyValuePair())
// implement configuration storing here, once needed
;
else
{
result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
return false;
}
break;
case FileStateProjectConfigurations:
if (line.GetTag().compare("EndGlobalSection") == 0)
this->Stack.pop();
else if (line.IsKeyValuePair())
// implement configuration storing here, once needed
;
else
{
result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
return false;
}
break;
case FileStateSolutionFilters:
if (line.GetTag().compare("EndGlobalSection") == 0)
this->Stack.pop();
else if (line.IsKeyValuePair())
// implement filter storing here, once needed
;
else
{
result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
return false;
}
break;
case FileStateGlobalSection:
if (line.GetTag().compare("EndGlobalSection") == 0)
this->Stack.pop();
else if (line.IsKeyValuePair())
// implement section storing here, once needed
;
else
{
result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
return false;
}
break;
case FileStateIgnore:
if (line.GetTag() == this->EndIgnoreTag)
{
this->Stack.pop();
this->EndIgnoreTag = "";
}
break;
default:
result.SetError(ResultErrorBadInternalState, this->GetCurrentLine());
return false;
}
return true;
}
//----------------------------------------------------------------------------
bool cmVisualStudioSlnParser::State::Finished(
cmVisualStudioSlnParser::ResultData& result)
{
if (this->Stack.top() != FileStateTopLevel)
{
result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
return false;
}
result.Result = ResultOK;
return true;
}
//----------------------------------------------------------------------------
void cmVisualStudioSlnParser::State::IgnoreUntilTag(const std::string& endTag)
{
this->Stack.push(FileStateIgnore);
this->EndIgnoreTag = endTag;
}
//----------------------------------------------------------------------------
cmVisualStudioSlnParser::ResultData::ResultData()
: Result(ResultOK)
, ResultLine(0)
{}
//----------------------------------------------------------------------------
void cmVisualStudioSlnParser::ResultData::Clear()
{
*this = ResultData();
}
//----------------------------------------------------------------------------
void cmVisualStudioSlnParser::ResultData::SetError(ParseResult error,
size_t line)
{
this->Result = error;
this->ResultLine = line;
}
//----------------------------------------------------------------------------
const cmVisualStudioSlnParser::DataGroupSet
cmVisualStudioSlnParser::DataGroupProjects(
1 << cmVisualStudioSlnParser::DataGroupProjectsBit);
const cmVisualStudioSlnParser::DataGroupSet
cmVisualStudioSlnParser::DataGroupProjectDependencies(
1 << cmVisualStudioSlnParser::DataGroupProjectDependenciesBit);
const cmVisualStudioSlnParser::DataGroupSet
cmVisualStudioSlnParser::DataGroupSolutionConfigurations(
1 << cmVisualStudioSlnParser::DataGroupSolutionConfigurationsBit);
const cmVisualStudioSlnParser::DataGroupSet
cmVisualStudioSlnParser::DataGroupProjectConfigurations(
1 << cmVisualStudioSlnParser::DataGroupProjectConfigurationsBit);
const cmVisualStudioSlnParser::DataGroupSet
cmVisualStudioSlnParser::DataGroupSolutionFilters(
1 << cmVisualStudioSlnParser::DataGroupSolutionFiltersBit);
const cmVisualStudioSlnParser::DataGroupSet
cmVisualStudioSlnParser::DataGroupGenericGlobalSections(
1 << cmVisualStudioSlnParser::DataGroupGenericGlobalSectionsBit);
const cmVisualStudioSlnParser::DataGroupSet
cmVisualStudioSlnParser::DataGroupAll(~0);
//----------------------------------------------------------------------------
bool cmVisualStudioSlnParser::Parse(std::istream& input,
cmSlnData& output,
DataGroupSet dataGroups)
{
this->LastResult.Clear();
if (!this->IsDataGroupSetSupported(dataGroups))
{
this->LastResult.SetError(ResultErrorUnsupportedDataGroup, 0);
return false;
}
State state(dataGroups);
return this->ParseImpl(input, output, state);
}
//----------------------------------------------------------------------------
bool cmVisualStudioSlnParser::ParseFile(const std::string& file,
cmSlnData& output,
DataGroupSet dataGroups)
{
this->LastResult.Clear();
if (!this->IsDataGroupSetSupported(dataGroups))
{
this->LastResult.SetError(ResultErrorUnsupportedDataGroup, 0);
return false;
}
std::ifstream f(file.c_str());
if (!f)
{
this->LastResult.SetError(ResultErrorOpeningInput, 0);
return false;
}
State state(dataGroups);
return this->ParseImpl(f, output, state);
}
//----------------------------------------------------------------------------
cmVisualStudioSlnParser::ParseResult
cmVisualStudioSlnParser::GetParseResult() const
{
return this->LastResult.Result;
}
//----------------------------------------------------------------------------
size_t cmVisualStudioSlnParser::GetParseResultLine() const
{
return this->LastResult.ResultLine;
}
//----------------------------------------------------------------------------
bool cmVisualStudioSlnParser::GetParseHadBOM() const
{
return this->LastResult.HadBOM;
}
//----------------------------------------------------------------------------
bool
cmVisualStudioSlnParser::IsDataGroupSetSupported(DataGroupSet dataGroups) const
{
return (dataGroups & DataGroupProjects) == dataGroups;
//only supporting DataGroupProjects for now
}
//----------------------------------------------------------------------------
bool cmVisualStudioSlnParser::ParseImpl(std::istream& input,
cmSlnData& output,
State& state)
{
std::string line;
// Does the .sln start with a Byte Order Mark?
if (!this->ParseBOM(input, line, state))
return false;
do
{
line = cmSystemTools::TrimWhitespace(line);
if (line.empty())
continue;
ParsedLine parsedLine;
switch (state.NextLineFormat())
{
case LineMultiValueTag:
if (!this->ParseMultiValueTag(line, parsedLine, state))
return false;
break;
case LineSingleValueTag:
if (!this->ParseSingleValueTag(line, parsedLine, state))
return false;
break;
case LineKeyValuePair:
if (!this->ParseKeyValuePair(line, parsedLine, state))
return false;
break;
case LineVerbatim:
parsedLine.CopyVerbatim(line);
break;
}
if (parsedLine.IsComment())
continue;
if (!state.Process(parsedLine, output, this->LastResult))
return false;
}
while (state.ReadLine(input, line));
return state.Finished(this->LastResult);
}
//----------------------------------------------------------------------------
bool cmVisualStudioSlnParser::ParseBOM(std::istream& input,
std::string& line,
State& state)
{
char bom[4];
if (!input.get(bom, 4))
{
this->LastResult.SetError(ResultErrorReadingInput, 1);
return false;
}
this->LastResult.HadBOM =
(bom[0] == char(0xEF) && bom[1] == char(0xBB) && bom[2] == char(0xBF));
if (!state.ReadLine(input, line))
{
this->LastResult.SetError(ResultErrorReadingInput, 1);
return false;
}
if (!this->LastResult.HadBOM)
line = bom + line; // it wasn't a BOM, prepend it to first line
return true;
}
//----------------------------------------------------------------------------
bool cmVisualStudioSlnParser::ParseMultiValueTag(const std::string& line,
ParsedLine& parsedLine,
State& state)
{
size_t idxEqualSign = line.find('=');
const std::string& fullTag = line.substr(0, idxEqualSign);
if (!this->ParseTag(fullTag, parsedLine, state))
return false;
if (idxEqualSign != line.npos)
{
size_t idxFieldStart = idxEqualSign + 1;
if (idxFieldStart < line.size())
{
size_t idxParsing = idxFieldStart;
bool inQuotes = false;
for (;;)
{
idxParsing = line.find_first_of(",\"", idxParsing);
bool fieldOver = false;
if (idxParsing == line.npos)
{
fieldOver = true;
if (inQuotes)
{
this->LastResult.SetError(ResultErrorInputStructure,
state.GetCurrentLine());
return false;
}
}
else if (line[idxParsing] == ',' && !inQuotes)
fieldOver = true;
else if (line[idxParsing] == '"')
inQuotes = !inQuotes;
if (fieldOver)
{
if (!this->ParseValue(line.substr(idxFieldStart,
idxParsing - idxFieldStart),
parsedLine))
return false;
if (idxParsing == line.npos)
break; //end of last field
idxFieldStart = idxParsing + 1;
}
++idxParsing;
}
}
}
return true;
}
//----------------------------------------------------------------------------
bool cmVisualStudioSlnParser::ParseSingleValueTag(const std::string& line,
ParsedLine& parsedLine,
State& state)
{
size_t idxEqualSign = line.find('=');
const std::string& fullTag = line.substr(0, idxEqualSign);
if (!this->ParseTag(fullTag, parsedLine, state))
return false;
if (idxEqualSign != line.npos)
{
if (!this->ParseValue(line.substr(idxEqualSign + 1), parsedLine))
return false;
}
return true;
}
//----------------------------------------------------------------------------
bool cmVisualStudioSlnParser::ParseKeyValuePair(const std::string& line,
ParsedLine& parsedLine,
State& /*state*/)
{
size_t idxEqualSign = line.find('=');
if (idxEqualSign == line.npos)
{
parsedLine.CopyVerbatim(line);
return true;
}
const std::string& key = line.substr(0, idxEqualSign);
parsedLine.SetTag(cmSystemTools::TrimWhitespace(key));
const std::string& value = line.substr(idxEqualSign + 1);
parsedLine.AddValue(cmSystemTools::TrimWhitespace(value));
return true;
}
//----------------------------------------------------------------------------
bool cmVisualStudioSlnParser::ParseTag(const std::string& fullTag,
ParsedLine& parsedLine,
State& state)
{
size_t idxLeftParen = fullTag.find('(');
if (idxLeftParen == fullTag.npos)
{
parsedLine.SetTag(cmSystemTools::TrimWhitespace(fullTag));
return true;
}
parsedLine.SetTag(
cmSystemTools::TrimWhitespace(fullTag.substr(0, idxLeftParen)));
size_t idxRightParen = fullTag.rfind(')');
if (idxRightParen == fullTag.npos)
{
this->LastResult.SetError(ResultErrorInputStructure,
state.GetCurrentLine());
return false;
}
const std::string& arg = cmSystemTools::TrimWhitespace(
fullTag.substr(idxLeftParen + 1, idxRightParen - idxLeftParen - 1));
if (arg[0] == '"')
{
if (arg[arg.size() - 1] != '"')
{
this->LastResult.SetError(ResultErrorInputStructure,
state.GetCurrentLine());
return false;
}
parsedLine.SetQuotedArg(arg.substr(1, arg.size() - 2));
}
else
parsedLine.SetArg(arg);
return true;
}
//----------------------------------------------------------------------------
bool cmVisualStudioSlnParser::ParseValue(const std::string& value,
ParsedLine& parsedLine)
{
const std::string& trimmed = cmSystemTools::TrimWhitespace(value);
if (trimmed.empty())
parsedLine.AddValue(trimmed);
else if (trimmed[0] == '"' && trimmed[trimmed.size() - 1] == '"')
parsedLine.AddQuotedValue(trimmed.substr(1, trimmed.size() - 2));
else
parsedLine.AddValue(trimmed);
return true;
}