361 lines
11 KiB
C++
361 lines
11 KiB
C++
|
|
// xscript-cli - XScript definition file validator and parser
|
||
|
|
//
|
||
|
|
// Usage:
|
||
|
|
// xscript-cli check <file.xscript> - Check syntax and validate references
|
||
|
|
// xscript-cli check-dir <directory> - Check all .xscript files in directory
|
||
|
|
// xscript-cli list-types <file.xscript> - List all types defined in file
|
||
|
|
// xscript-cli info <file.xscript> <type> - Show info about a specific type
|
||
|
|
|
||
|
|
#include <QCoreApplication>
|
||
|
|
#include <QCommandLineParser>
|
||
|
|
#include <QFile>
|
||
|
|
#include <QFileInfo>
|
||
|
|
#include <QDir>
|
||
|
|
#include <QDirIterator>
|
||
|
|
#include <QTextStream>
|
||
|
|
|
||
|
|
#include "lexer.h"
|
||
|
|
#include "parser.h"
|
||
|
|
#include "typeregistry.h"
|
||
|
|
|
||
|
|
static QTextStream out(stdout);
|
||
|
|
static QTextStream err(stderr);
|
||
|
|
|
||
|
|
// Color codes for terminal output
|
||
|
|
namespace Color {
|
||
|
|
const char* Reset = "\033[0m";
|
||
|
|
const char* Red = "\033[31m";
|
||
|
|
const char* Green = "\033[32m";
|
||
|
|
const char* Yellow = "\033[33m";
|
||
|
|
const char* Blue = "\033[34m";
|
||
|
|
const char* Cyan = "\033[36m";
|
||
|
|
const char* Bold = "\033[1m";
|
||
|
|
}
|
||
|
|
|
||
|
|
struct CheckResult {
|
||
|
|
QString filePath;
|
||
|
|
QString fileName;
|
||
|
|
bool success = false;
|
||
|
|
QString errorMessage;
|
||
|
|
QStringList typeNames;
|
||
|
|
int warningCount = 0;
|
||
|
|
QStringList warnings;
|
||
|
|
};
|
||
|
|
|
||
|
|
CheckResult checkFile(const QString& filePath) {
|
||
|
|
CheckResult result;
|
||
|
|
result.filePath = filePath;
|
||
|
|
result.fileName = QFileInfo(filePath).fileName();
|
||
|
|
|
||
|
|
QFile file(filePath);
|
||
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||
|
|
result.errorMessage = "Failed to open file";
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
QString content = QString::fromUtf8(file.readAll());
|
||
|
|
file.close();
|
||
|
|
|
||
|
|
try {
|
||
|
|
Lexer lexer(content);
|
||
|
|
Parser parser(std::move(lexer));
|
||
|
|
Module module = parser.parseModule();
|
||
|
|
|
||
|
|
result.success = true;
|
||
|
|
|
||
|
|
// Collect type names
|
||
|
|
for (auto it = module.types.begin(); it != module.types.end(); ++it) {
|
||
|
|
result.typeNames.append(it.key());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for potential issues (warnings)
|
||
|
|
for (auto it = module.types.begin(); it != module.types.end(); ++it) {
|
||
|
|
const TypeDef& td = it.value();
|
||
|
|
|
||
|
|
// Warn if root type has no criteria
|
||
|
|
if (td.isRoot && td.criteria.isEmpty()) {
|
||
|
|
result.warnings.append(QString("Type '%1' is root but has no criteria block").arg(td.name));
|
||
|
|
result.warningCount++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Warn if type has no body
|
||
|
|
if (td.body.isEmpty()) {
|
||
|
|
result.warnings.append(QString("Type '%1' has an empty body").arg(td.name));
|
||
|
|
result.warningCount++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
} catch (const std::exception& e) {
|
||
|
|
result.errorMessage = QString::fromStdString(e.what());
|
||
|
|
}
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
int cmdCheck(const QString& filePath) {
|
||
|
|
CheckResult result = checkFile(filePath);
|
||
|
|
|
||
|
|
if (result.success) {
|
||
|
|
out << Color::Green << "OK" << Color::Reset << " " << result.fileName;
|
||
|
|
if (!result.typeNames.isEmpty()) {
|
||
|
|
out << " (" << result.typeNames.size() << " type(s))";
|
||
|
|
}
|
||
|
|
out << "\n";
|
||
|
|
|
||
|
|
for (const QString& warning : result.warnings) {
|
||
|
|
out << Color::Yellow << " WARNING: " << Color::Reset << warning << "\n";
|
||
|
|
}
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
} else {
|
||
|
|
err << Color::Red << "ERROR" << Color::Reset << " " << result.fileName << "\n";
|
||
|
|
err << " " << result.errorMessage << "\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int cmdCheckDir(const QString& dirPath) {
|
||
|
|
QDir dir(dirPath);
|
||
|
|
if (!dir.exists()) {
|
||
|
|
err << Color::Red << "Error: Directory does not exist: " << dirPath << Color::Reset << "\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
QDirIterator it(dirPath, {"*.xscript"}, QDir::Files, QDirIterator::Subdirectories);
|
||
|
|
|
||
|
|
int totalFiles = 0;
|
||
|
|
int successCount = 0;
|
||
|
|
int errorCount = 0;
|
||
|
|
int warningCount = 0;
|
||
|
|
QStringList allTypes;
|
||
|
|
QStringList failedFiles;
|
||
|
|
|
||
|
|
while (it.hasNext()) {
|
||
|
|
QString filePath = it.next();
|
||
|
|
totalFiles++;
|
||
|
|
|
||
|
|
CheckResult result = checkFile(filePath);
|
||
|
|
|
||
|
|
// Show relative path from the directory
|
||
|
|
QString relativePath = dir.relativeFilePath(filePath);
|
||
|
|
|
||
|
|
if (result.success) {
|
||
|
|
successCount++;
|
||
|
|
out << Color::Green << "OK" << Color::Reset << " " << relativePath;
|
||
|
|
if (!result.typeNames.isEmpty()) {
|
||
|
|
out << " (" << result.typeNames.size() << " type(s))";
|
||
|
|
allTypes.append(result.typeNames);
|
||
|
|
}
|
||
|
|
out << "\n";
|
||
|
|
|
||
|
|
warningCount += result.warningCount;
|
||
|
|
for (const QString& warning : result.warnings) {
|
||
|
|
out << Color::Yellow << " WARNING: " << Color::Reset << warning << "\n";
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
errorCount++;
|
||
|
|
failedFiles.append(relativePath);
|
||
|
|
err << Color::Red << "ERROR" << Color::Reset << " " << relativePath << "\n";
|
||
|
|
err << " " << result.errorMessage << "\n";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Summary
|
||
|
|
out << "\n" << Color::Bold << "Summary:" << Color::Reset << "\n";
|
||
|
|
out << " Files checked: " << totalFiles << "\n";
|
||
|
|
out << " " << Color::Green << "Passed: " << successCount << Color::Reset << "\n";
|
||
|
|
if (errorCount > 0) {
|
||
|
|
out << " " << Color::Red << "Failed: " << errorCount << Color::Reset << "\n";
|
||
|
|
}
|
||
|
|
if (warningCount > 0) {
|
||
|
|
out << " " << Color::Yellow << "Warnings: " << warningCount << Color::Reset << "\n";
|
||
|
|
}
|
||
|
|
out << " Total types: " << allTypes.size() << "\n";
|
||
|
|
|
||
|
|
// Validate cross-file references
|
||
|
|
if (!allTypes.isEmpty()) {
|
||
|
|
TypeRegistry registry;
|
||
|
|
int refErrors = 0;
|
||
|
|
|
||
|
|
// Re-load all files into a single registry
|
||
|
|
QDirIterator it2(dirPath, {"*.xscript"}, QDir::Files, QDirIterator::Subdirectories);
|
||
|
|
while (it2.hasNext()) {
|
||
|
|
QString filePath = it2.next();
|
||
|
|
QFile file(filePath);
|
||
|
|
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||
|
|
try {
|
||
|
|
registry.ingestScript(QString::fromUtf8(file.readAll()), filePath);
|
||
|
|
} catch (...) {
|
||
|
|
// Already reported
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
QStringList refErrorList = registry.validateTypeReferences();
|
||
|
|
if (!refErrorList.isEmpty()) {
|
||
|
|
out << "\n" << Color::Red << "Reference errors:" << Color::Reset << "\n";
|
||
|
|
for (const QString& refErr : refErrorList) {
|
||
|
|
out << " " << refErr << "\n";
|
||
|
|
refErrors++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (refErrors > 0) {
|
||
|
|
errorCount += refErrors;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (errorCount > 0) ? 1 : 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
int cmdListTypes(const QString& filePath) {
|
||
|
|
CheckResult result = checkFile(filePath);
|
||
|
|
|
||
|
|
if (!result.success) {
|
||
|
|
err << Color::Red << "Error parsing file:" << Color::Reset << "\n";
|
||
|
|
err << " " << result.errorMessage << "\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
out << Color::Bold << "Types in " << result.fileName << ":" << Color::Reset << "\n";
|
||
|
|
|
||
|
|
// Re-parse to get more details
|
||
|
|
QFile file(filePath);
|
||
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
QString content = QString::fromUtf8(file.readAll());
|
||
|
|
Lexer lexer(content);
|
||
|
|
Parser parser(std::move(lexer));
|
||
|
|
Module module = parser.parseModule();
|
||
|
|
|
||
|
|
for (auto it = module.types.begin(); it != module.types.end(); ++it) {
|
||
|
|
const TypeDef& td = it.value();
|
||
|
|
|
||
|
|
out << " " << Color::Cyan << td.name << Color::Reset;
|
||
|
|
|
||
|
|
QStringList flags;
|
||
|
|
if (td.isRoot) flags.append("root");
|
||
|
|
if (!td.display.isEmpty()) flags.append(QString("display=\"%1\"").arg(td.display));
|
||
|
|
if (td.order == ByteOrder::BE) flags.append("BE");
|
||
|
|
else flags.append("LE");
|
||
|
|
|
||
|
|
if (!flags.isEmpty()) {
|
||
|
|
out << " [" << flags.join(", ") << "]";
|
||
|
|
}
|
||
|
|
out << "\n";
|
||
|
|
|
||
|
|
if (!td.criteria.isEmpty()) {
|
||
|
|
out << " criteria: " << td.criteria.size() << " statement(s)\n";
|
||
|
|
}
|
||
|
|
out << " body: " << td.body.size() << " statement(s)\n";
|
||
|
|
}
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
int cmdInfo(const QString& filePath, const QString& typeName) {
|
||
|
|
QFile file(filePath);
|
||
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||
|
|
err << Color::Red << "Error: Failed to open file" << Color::Reset << "\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
QString content = QString::fromUtf8(file.readAll());
|
||
|
|
|
||
|
|
try {
|
||
|
|
Lexer lexer(content);
|
||
|
|
Parser parser(std::move(lexer));
|
||
|
|
Module module = parser.parseModule();
|
||
|
|
|
||
|
|
if (!module.types.contains(typeName)) {
|
||
|
|
err << Color::Red << "Error: Type '" << typeName << "' not found" << Color::Reset << "\n";
|
||
|
|
err << "Available types:\n";
|
||
|
|
for (const QString& name : module.types.keys()) {
|
||
|
|
err << " " << name << "\n";
|
||
|
|
}
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
const TypeDef& td = module.types[typeName];
|
||
|
|
|
||
|
|
out << Color::Bold << "Type: " << td.name << Color::Reset << "\n";
|
||
|
|
out << " Root: " << (td.isRoot ? "yes" : "no") << "\n";
|
||
|
|
out << " Display: " << (td.display.isEmpty() ? "(none)" : td.display) << "\n";
|
||
|
|
out << " Byte order: " << (td.order == ByteOrder::BE ? "big-endian" : "little-endian") << "\n";
|
||
|
|
out << " Criteria statements: " << td.criteria.size() << "\n";
|
||
|
|
out << " Body statements: " << td.body.size() << "\n";
|
||
|
|
|
||
|
|
} catch (const std::exception& e) {
|
||
|
|
err << Color::Red << "Error parsing file: " << e.what() << Color::Reset << "\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
int main(int argc, char *argv[]) {
|
||
|
|
QCoreApplication app(argc, argv);
|
||
|
|
QCoreApplication::setApplicationName("xscript-cli");
|
||
|
|
QCoreApplication::setApplicationVersion("1.0.0");
|
||
|
|
|
||
|
|
QCommandLineParser parser;
|
||
|
|
parser.setApplicationDescription("XScript definition file validator and parser");
|
||
|
|
parser.addHelpOption();
|
||
|
|
parser.addVersionOption();
|
||
|
|
|
||
|
|
parser.addPositionalArgument("command", "Command to run: check, check-dir, list-types, info");
|
||
|
|
parser.addPositionalArgument("args", "Command arguments", "[args...]");
|
||
|
|
|
||
|
|
parser.process(app);
|
||
|
|
|
||
|
|
const QStringList args = parser.positionalArguments();
|
||
|
|
|
||
|
|
if (args.isEmpty()) {
|
||
|
|
err << "Error: No command specified\n\n";
|
||
|
|
err << "Usage:\n";
|
||
|
|
err << " xscript-cli check <file.xscript> - Check syntax and validate\n";
|
||
|
|
err << " xscript-cli check-dir <directory> - Check all .xscript files\n";
|
||
|
|
err << " xscript-cli list-types <file.xscript> - List all types in file\n";
|
||
|
|
err << " xscript-cli info <file.xscript> <type> - Show type info\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
const QString command = args[0];
|
||
|
|
|
||
|
|
if (command == "check") {
|
||
|
|
if (args.size() < 2) {
|
||
|
|
err << "Error: check requires a file path\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
return cmdCheck(args[1]);
|
||
|
|
}
|
||
|
|
else if (command == "check-dir") {
|
||
|
|
if (args.size() < 2) {
|
||
|
|
err << "Error: check-dir requires a directory path\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
return cmdCheckDir(args[1]);
|
||
|
|
}
|
||
|
|
else if (command == "list-types") {
|
||
|
|
if (args.size() < 2) {
|
||
|
|
err << "Error: list-types requires a file path\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
return cmdListTypes(args[1]);
|
||
|
|
}
|
||
|
|
else if (command == "info") {
|
||
|
|
if (args.size() < 3) {
|
||
|
|
err << "Error: info requires a file path and type name\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
return cmdInfo(args[1], args[2]);
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
err << "Error: Unknown command '" << command << "'\n";
|
||
|
|
err << "Valid commands: check, check-dir, list-types, info\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
}
|