From 04bfcd50e8197a0a59a006b743f89aa18d289e3d Mon Sep 17 00:00:00 2001 From: Canrad <1517807724@qq.com> Date: Thu, 29 Oct 2026 23:32:30 +0800 Subject: [PATCH] =?UTF-8?q?refactor(cli):=20cli=20=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E6=95=B4=E4=BD=93=E9=80=80=E5=BD=B9=E2=80=94=E2=80=94=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E8=BD=AE=E5=BD=92=20libca=20ca::opt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ArgParser(458 行声明式解析器)与 libca ca::opt 平行造轮(子命令嵌套/ 互斥组/类型化选项/CJK 宽度 help 排版全面落后),且全仓零产品消费方: mjt 唯一在用的是 OutputSink 与 kCliI18nTableSet 两小块(已先行移交/ 摘除,mjt#29),keep_cli/cli_runner/ArgParser 零触达。 连带退役整条死 i18n 链:cli/translations(42 个 mbase.cli.* 键只服务 ArgParser 文案)、生成头回源 rule(i18n_src_sync)、i18n.hpp 兜底门 (mjbase::tr 全仓零消费方,mjt 自带表自注册)、i18n_test。 - 删 src/mjbase/cli(6 文件)+ test/unittest/cli(4 文件)+ test/unittest/i18n + src/mjbase/i18n.hpp + 生成头 - xmake 摘对应 src/test glob、embed-lang 行与同步 rule - 套件 1871 绿 + 3 既有 POSIX 跳过(1922 − 48 逐位对账) --- mjbase/src/mjbase/cli/arg_parser.hpp | 458 ------------------ mjbase/src/mjbase/cli/cli_runner.cpp | 300 ------------ mjbase/src/mjbase/cli/cli_runner.hpp | 143 ------ mjbase/src/mjbase/cli/i18n_tables.hpp | 30 -- mjbase/src/mjbase/cli/keep_cli.hpp | 79 --- mjbase/src/mjbase/cli/output_sink.hpp | 91 ---- mjbase/src/mjbase/cli/translations/en_US.lang | 144 ------ mjbase/src/mjbase/cli/translations/zh_CN.lang | 145 ------ mjbase/src/mjbase/i18n.hpp | 36 -- mjbase/test/unittest/cli/arg_parser_test.cpp | 286 ----------- mjbase/test/unittest/cli/cli_runner_test.cpp | 381 --------------- mjbase/test/unittest/cli/keep_cli_test.cpp | 123 ----- mjbase/test/unittest/cli/output_sink_test.cpp | 87 ---- mjbase/test/unittest/i18n/i18n_test.cpp | 23 - mjbase/xmake.lua | 35 +- 15 files changed, 4 insertions(+), 2357 deletions(-) delete mode 100644 mjbase/src/mjbase/cli/arg_parser.hpp delete mode 100644 mjbase/src/mjbase/cli/cli_runner.cpp delete mode 100644 mjbase/src/mjbase/cli/cli_runner.hpp delete mode 100644 mjbase/src/mjbase/cli/i18n_tables.hpp delete mode 100644 mjbase/src/mjbase/cli/keep_cli.hpp delete mode 100644 mjbase/src/mjbase/cli/output_sink.hpp delete mode 100644 mjbase/src/mjbase/cli/translations/en_US.lang delete mode 100644 mjbase/src/mjbase/cli/translations/zh_CN.lang delete mode 100644 mjbase/src/mjbase/i18n.hpp delete mode 100644 mjbase/test/unittest/cli/arg_parser_test.cpp delete mode 100644 mjbase/test/unittest/cli/cli_runner_test.cpp delete mode 100644 mjbase/test/unittest/cli/keep_cli_test.cpp delete mode 100644 mjbase/test/unittest/cli/output_sink_test.cpp delete mode 100644 mjbase/test/unittest/i18n/i18n_test.cpp diff --git a/mjbase/src/mjbase/cli/arg_parser.hpp b/mjbase/src/mjbase/cli/arg_parser.hpp deleted file mode 100644 index dc55c11..0000000 --- a/mjbase/src/mjbase/cli/arg_parser.hpp +++ /dev/null @@ -1,458 +0,0 @@ -#pragma once - -// ── 声明式 CLI 参数解析器(header-only)──────────────────────────── -// -// 消除各线 cmd_*.cpp 与 pass 驱动里重复的「for(i..argc) strcmp(argv) { -// if(++i>=argc) err } + help 文本 + 必填校验」模板。声明式注册 option 表, -// 统一支持 -x v / --name v / --name=v 三种写法,自动生成 help,自动校验必填。 -// -// 用法: -// mjbase::cli::ArgParser p("mjbase opt -i -o ", "处理字节码"); -// p.add({"--input","-i"}, mjbase::cli::OptKind::String, "", "输入 jar/class", true) -// .add({"--output","-o"}, mjbase::cli::OptKind::String, "", "输出路径", true) -// .add({"--verbose","-v"}, mjbase::cli::OptKind::Flag, "", "详细输出"); -// auto r = p.parse(argc, argv); -// if (!r) return 1; // 错误或 -h 已打印,直接退出 -// auto in = r->getString("--input"); -// -// 设计约束:不依赖 RTTI/异常;纯标准库 + libca 类型(遵循 AGENTS.md)。 -// 框架文案走 mbase.cli.* 单源 key, -// .lang 表随 mjbase target 原样嵌入,见 cli/i18n_tables.hpp)。 - -#include -#include -#include -#include -#include -#include -#include - -#include "libca/i18n/i18n.hpp" - -namespace mjbase::cli { - -/// Option 取值类型。 -enum class OptKind -{ - Flag, // 无值开关:-v / --verbose - String, // 单字符串:-i jar - Int, // 整数:--length 16 - CommaList, // 逗号分隔列表:-p A,B,C(也接受 -p A -p B 多次追加) - Positional, // 位置参数(裸 argv,非 - 开头):grep -}; - -/// 一个 option 的声明。 -struct OptSpec -{ - std::vector names; // 别名列表;第一个为 canonical 名(约定长名 --xxx 在前) - OptKind kind = OptKind::String; - std::string metavar; // help 里的占位符 ;Positional 用作位置参数名 - std::string help; // 该 option 描述 - bool required = false; // 缺失则报错(Flag/Positional 忽略此字段) -}; - -/// 解析结果。所有 getter 按 canonical 名(OptSpec.names[0])取值。 -class ArgResult -{ -public: - ArgResult() = default; - - /// Flag 是否出现。 - bool has(const std::string& canonical) const - { - return present_.find(canonical) != present_.end(); - } - - /// 取 String 值;未出现返回 def。 - std::string getString(const std::string& canonical, std::string def = {}) const - { - auto it = strValues_.find(canonical); - return it == strValues_.end() ? def : it->second; - } - - /// 取 Int 值;未出现或解析失败返回 def。 - int getInt(const std::string& canonical, int def = 0) const - { - auto it = intValues_.find(canonical); - return it == intValues_.end() ? def : it->second; - } - - /// 取 CommaList 值;未出现返回空。多次出现会累加。 - std::vector getList(const std::string& canonical) const - { - auto it = listValues_.find(canonical); - return it == listValues_.end() ? std::vector{} : it->second; - } - - /// 所有位置参数(按命令行顺序)。 - const std::vector& positionals() const { return positionals_; } - - // ── 内部累积接口(ArgParser::parse 用)── - void markPresent(const std::string& canonical) { present_.insert(canonical); } - void setString(const std::string& canonical, std::string v) - { - strValues_[canonical] = std::move(v); - present_.insert(canonical); - } - void setInt(const std::string& canonical, int v) - { - intValues_[canonical] = v; - present_.insert(canonical); - } - void appendList(const std::string& canonical, std::vector items) - { - auto& dst = listValues_[canonical]; - for (auto& it : items) dst.push_back(std::move(it)); - present_.insert(canonical); - } - void appendPositional(std::string v) { positionals_.push_back(std::move(v)); } - -private: - std::unordered_map strValues_; - std::unordered_map intValues_; - std::unordered_map> listValues_; - std::unordered_set present_; // 仅用 key 集合 - std::vector positionals_; -}; - -/// ArgParser 解析失败的稳定类别。 -/// -/// 现有调用方可继续使用 parse() 的 optional 接口;需要稳定退出码的命令应使用 -/// parseDetailed(),而不要从错误文本推断失败原因。 -enum class ArgParseError -{ - None, - HelpRequested, - UnknownOption, - FlagHasValue, - MissingValue, - EmptyValue, - UnexpectedArgument, - MissingRequired, - InvalidInteger, - MutexConflict, - MutexRequired, -}; - -/// ArgParser 的带分类解析结果。 -class ArgParseOutcome -{ -public: - /// 解析是否成功。 - bool ok() const { return value_.has_value(); } - - /// 是否因 help 请求提前结束。 - bool helpRequested() const { return error_ == ArgParseError::HelpRequested; } - - /// 失败类别;成功时为 None。 - ArgParseError error() const { return error_; } - - /// 成功后的不可变参数结果;失败时为 nullptr。 - const ArgResult* value() const { return value_ ? &*value_ : nullptr; } - - /// 转移成功的参数结果,供保持 optional API 的兼容包装使用。 - std::optional takeValue() { return std::move(value_); } - -private: - friend class ArgParser; - - static ArgParseOutcome success(ArgResult value) - { - ArgParseOutcome outcome; - outcome.value_ = std::move(value); - return outcome; - } - - static ArgParseOutcome failure(ArgParseError error) - { - ArgParseOutcome outcome; - outcome.error_ = error; - return outcome; - } - - std::optional value_; - ArgParseError error_ = ArgParseError::None; -}; - -/// 声明式参数解析器。 -class ArgParser -{ -public: - ArgParser(std::string usageLine, std::string description) - : usageLine_(std::move(usageLine)) - , description_(std::move(description)) - { - // 内置 -h/--help,不占 user spec 位 - } - - /// 链式注册。OptSpec.names[0] 为 canonical 名。 - ArgParser& add(OptSpec spec) - { - // 第一个 spec 决定 canonical 名;建立别名→canonical 映射 - if (spec.names.empty()) return *this; - std::string canonical = spec.names[0]; - for (const auto& nm : spec.names) alias_[nm] = canonical; - specs_.push_back(std::move(spec)); - return *this; - } - /// 便捷重载:从 names+kind 构造。 - ArgParser& add(std::vector names, OptKind kind, std::string metavar, - std::string help, bool required = false) - { - return add(OptSpec{std::move(names), kind, std::move(metavar), std::move(help), required}); - } - - /// 注册互斥组:组内 canonical 名最多出现一个。 - /// @param canonicalNames 组内成员的 canonical 名(OptSpec.names[0]) - /// @param required true 表示组内至少出现一个(如 subs 的 -s/--interface 二选一) - ArgParser& addMutexGroup(std::vector canonicalNames, bool required = false) - { - mutexGroups_.push_back({std::move(canonicalNames), required}); - return *this; - } - - /// 打印 help(自动从 specs 拼接)。usage 行 + 描述 + options 表。 - /// 框架文案经 mbase.cli.* 单源表本地化(mjbase 自带 .lang 嵌入,显式 init 接入, - /// 见 cli/i18n_tables.hpp);OptSpec.help/metavar 由调用方在注册时用 tr/trf 渲染。 - void printHelp() const - { - std::fprintf(stdout, "%s:\n %s\n\n", ca::i18n::tr("mbase.cli.usage"), usageLine_.c_str()); - if (!description_.empty()) { - std::fprintf(stdout, "%s\n\n", description_.c_str()); - } - std::fprintf(stdout, "%s\n", ca::i18n::tr("mbase.cli.options_header")); - std::fprintf(stdout, " -h, --help%*s%s\n", 16, "", ca::i18n::tr("mbase.cli.help_flag")); - for (const auto& s : specs_) { - printOptionRow(s); - } - } - - /// 解析 argv[1..argc),并保留失败类别。 - /// - /// 失败(未知参数/缺值/缺必填/-h)已打印信息到 stdout/stderr;需要区分 help 和 - /// 参数错误的调用方使用 ArgParseOutcome::error() 决定退出码。 - ArgParseOutcome parseDetailed(int argc, char** argv) const - { - ArgResult result; - for (int i = 1; i < argc; i++) { - std::string arg = argv[i]; - - // help:任何位置出现都触发 - if (arg == "-h" || arg == "--help") { - printHelp(); - return ArgParseOutcome::failure(ArgParseError::HelpRequested); - } - - // 是否以 - 开头(option)还是位置参数 - if (arg.size() >= 1 && arg[0] == '-' && arg != "-") { - // 拆出可能的 =value - std::string name, inlineVal; - bool hasInline = false; - auto eq = arg.find('='); - if (eq != std::string::npos) { - name = arg.substr(0, eq); - inlineVal = arg.substr(eq + 1); - hasInline = true; - } - else { - name = arg; - } - - const OptSpec* spec = findSpec(name); - if (!spec) { - std::fprintf(stderr, "Error: %s\n", - ca::i18n::trf("mbase.cli.err.unknown_option", {arg}).c_str()); - return ArgParseOutcome::failure(ArgParseError::UnknownOption); - } - std::string canonical = spec->names[0]; - - if (spec->kind == OptKind::Flag) { - if (hasInline) { - std::fprintf(stderr, "Error: %s\n", - ca::i18n::trf("mbase.cli.err.flag_no_value", {name}).c_str()); - return ArgParseOutcome::failure(ArgParseError::FlagHasValue); - } - result.markPresent(canonical); - continue; - } - - // 取值:优先 inline,否则取下一个 argv - std::string value; - if (hasInline) { - value = inlineVal; - } - else { - if (i + 1 >= argc) { - std::fprintf(stderr, "Error: %s\n", - ca::i18n::trf("mbase.cli.err.requires_argument", - {name}).c_str()); - return ArgParseOutcome::failure(ArgParseError::MissingValue); - } - value = argv[++i]; - } - - if (value.empty()) { - std::fprintf(stderr, "Error: %s\n", - ca::i18n::trf("mbase.cli.err.empty_argument", {name}).c_str()); - return ArgParseOutcome::failure(ArgParseError::EmptyValue); - } - - if (!consumeValue(*spec, canonical, value, result)) { - return ArgParseOutcome::failure(spec->kind == OptKind::Int - ? ArgParseError::InvalidInteger - : ArgParseError::MissingValue); - } - } - else { - // 位置参数:必须有对应的 Positional spec(否则报错,避免静默吞) - if (!hasPositionalSpec()) { - std::fprintf(stderr, "Error: %s\n", - ca::i18n::trf("mbase.cli.err.unexpected_argument", - {arg}).c_str()); - return ArgParseOutcome::failure(ArgParseError::UnexpectedArgument); - } - result.appendPositional(arg); - } - } - - // 必填校验 - for (const auto& s : specs_) { - if (s.required && s.kind != OptKind::Positional && s.kind != OptKind::Flag) { - std::string canonical = s.names[0]; - if (!result.has(canonical)) { - std::fprintf(stderr, "Error: %s\n", - ca::i18n::trf("mbase.cli.err.missing_required", - {canonical}).c_str()); - return ArgParseOutcome::failure(ArgParseError::MissingRequired); - } - } - } - - // 互斥组校验:组内最多一个;required 组至少一个 - for (const auto& g : mutexGroups_) { - int count = 0; - for (const auto& cn : g.names) { - if (result.has(cn)) count++; - } - if (count > 1) { - std::fprintf(stderr, "Error: %s\n", ca::i18n::tr("mbase.cli.err.mutex_conflict")); - return ArgParseOutcome::failure(ArgParseError::MutexConflict); - } - if (g.required && count == 0) { - std::fprintf(stderr, "Error: %s\n", ca::i18n::tr("mbase.cli.err.mutex_required")); - return ArgParseOutcome::failure(ArgParseError::MutexRequired); - } - } - - return ArgParseOutcome::success(std::move(result)); - } - - /// 兼容旧调用方的 optional 接口。 - std::optional parse(int argc, char** argv) const - { - return parseDetailed(argc, argv).takeValue(); - } - -private: - struct MutexGroup - { - std::vector names; - bool required; - }; - std::string usageLine_; - std::string description_; - std::vector specs_; - std::unordered_map alias_; // 别名→canonical - std::vector mutexGroups_; - - const OptSpec* findSpec(const std::string& name) const - { - auto it = alias_.find(name); - if (it == alias_.end()) return nullptr; - const std::string& canonical = it->second; - for (const auto& s : specs_) { - if (s.names[0] == canonical) return &s; - } - return nullptr; - } - - bool hasPositionalSpec() const - { - for (const auto& s : specs_) { - if (s.kind == OptKind::Positional) return true; - } - return false; - } - - bool consumeValue(const OptSpec& spec, const std::string& canonical, const std::string& value, - ArgResult& result) const - { - switch (spec.kind) { - case OptKind::String: result.setString(canonical, value); return true; - case OptKind::Int: - { - try { - int v = std::stoi(value); - result.setInt(canonical, v); - } - catch (...) { - std::fprintf(stderr, "Error: %s\n", - ca::i18n::trf("mbase.cli.err.invalid_integer", - {canonical, value}).c_str()); - return false; - } - return true; - } - case OptKind::CommaList: - { - result.appendList(canonical, splitComma(value)); - return true; - } - default: // Flag/Positional 不应走到这里 - result.setString(canonical, value); - return true; - } - } - - static std::vector splitComma(const std::string& s) - { - std::vector out; - if (s.empty()) return out; - std::size_t start = 0; - while (true) { - auto comma = s.find(',', start); - if (comma == std::string::npos) { - out.push_back(s.substr(start)); - break; - } - out.push_back(s.substr(start, comma - start)); - start = comma + 1; - } - return out; - } - - void printOptionRow(const OptSpec& s) const - { - // 拼接所有别名,如 "-i, --input" - std::string left; - for (std::size_t k = 0; k < s.names.size(); k++) { - if (k) left += ", "; - left += s.names[k]; - } - if (!s.metavar.empty()) { - left += " " + s.metavar; - } - // 对齐到 26 列(与 -h 行宽度接近),超过则换行缩进 - const int align = 26; - std::string pad; - if (static_cast(left.size()) < align) { - pad = std::string(align - left.size(), ' '); - } - else { - pad = "\n" + std::string(align, ' '); - } - std::fprintf(stdout, " %s%s%s\n", left.c_str(), pad.c_str(), s.help.c_str()); - } -}; - -} // namespace mjbase::cli diff --git a/mjbase/src/mjbase/cli/cli_runner.cpp b/mjbase/src/mjbase/cli/cli_runner.cpp deleted file mode 100644 index 0472b29..0000000 --- a/mjbase/src/mjbase/cli/cli_runner.cpp +++ /dev/null @@ -1,300 +0,0 @@ -#include "mjbase/cli/cli_runner.hpp" - -#include -#include -#include - -#include "jbc/core/reader.hpp" -#include "libca/fs/file_util.hpp" -#include "mjbase/i18n.hpp" -#include "mjbase/jar/jar_util.hpp" -#include "mjbase/model/resource_container.hpp" -#include "mjbase/model/resource_helper.hpp" -#include "mjbase/model/resource_model.hpp" - -namespace mjbase::cli { - -namespace { - -using ca::fs::FileUtil; -using mjbase::tr; - -namespace model = mjbase::model; - -// 判断输入是 jar、class 还是其它 -enum class InputKind -{ - Jar, - Class, - Unsupported -}; - -void setFailure(CliRunReport& report, CliFailureKind kind, std::string message) -{ - report.failure = kind; - report.failureMessage = std::move(message); -} - -// 重新导入输出文件,确认其中每个 Class 资源都能被 classfile 读取器完整解析 -// (mbase 侧以 ClassReader 异常判定;mjbase 换 jbc::core::readClass—— -// Err / 非 Validated / 类名为空都视为验证失败)。 -bool verifyOutput(const std::string& output, InputKind kind) -{ - model::ResourceContainer check; - if (kind == InputKind::Jar) { - if (!model::ResourceHelper::importFromJar(check, output).is_ok()) return false; - } - else if (kind == InputKind::Class) { - if (!model::ResourceHelper::importFromClassFile(check, output).is_ok()) - return false; - } - else { - return false; - } - bool hasClass = false; - for (const auto& [uri, resource] : check.getResources()) { - if (!resource) { - std::fprintf(stderr, "[verify] null resource for %s\n", - std::string(uri).c_str()); - return false; - } - if (resource->getType() != model::ResourceType::Class) { - // 病态 jar 可能含 0 字节 / 非 class 内容但以 .class 命名的 entry - // (如被混淆器改坏的 module-info.class)。此时按内容分拣已是 - // UnknownResource,说明其字节本身不构成可解析 class,跳过即可。 - continue; - } - - const auto* classResource = - ca::core::tag_cast::dyn_cast(resource.get()); - if (!classResource || classResource->get().empty()) { - std::fprintf(stderr, "[verify] empty class bytes for %s\n", - std::string(uri).c_str()); - return false; - } - - const auto& bytes = classResource->get(); - auto read = jbc::core::readClass(jbc::core::ByteView(bytes)); - if (read.is_err()) { - std::fprintf(stderr, "[verify] parse failed for %s: %s\n", - std::string(uri).c_str(), - jbc::core::to_string(std::move(read).unwrap_err())); - return false; - } - auto readResult = std::move(read).unwrap(); // ReadResult 持 unique_ptr,只能移出 - if (readResult.state != jbc::core::ParseState::Validated) { - std::fprintf(stderr, "[verify] degraded class for %s: %s\n", - std::string(uri).c_str(), jbc::core::to_string(readResult.state)); - return false; - } - if (readResult.file->thisName().empty()) { - std::fprintf(stderr, "[verify] empty class name for %s\n", - std::string(uri).c_str()); - return false; - } - hasClass = true; - } - return hasClass; -} - -bool validateOutputTarget(const CliOptions& options, CliRunReport& report) -{ - if (!FileUtil::exists(options.output)) return true; - setFailure(report, CliFailureKind::InvalidArguments, - tr("mbase.cli.runner.err.output_exists")); - std::fprintf(stderr, "Error: refusing to overwrite existing output %s\n", - options.output.c_str()); - return false; -} - -std::string createStagedOutput(const std::string& output) -{ - // output 持 argv 值(D36 UTF-8 契约):显式 u8path(u8path 清扫);base/回传 - // 串一律 generic_u8string 保持 UTF-8 字节域——原 generic_string 按 ACP 再编码, - // 非 UTF-8 ACP 进程里 UTF-8 路径会被转 mojibake 且 create_file(libca UTF-8 - // 契约)二次解码放大。 - const std::filesystem::path destination = std::filesystem::u8path(output); - const auto parent = destination.has_parent_path() ? destination.parent_path() - : std::filesystem::path("."); - const auto base = destination.filename().generic_u8string(); - static std::atomic sequence{0}; - const auto timestamp = static_cast( - std::chrono::steady_clock::now().time_since_epoch().count()); - for (ca::u32 attempt = 0; attempt < 128; ++attempt) { - const auto suffix = std::to_string(timestamp) + "." + - std::to_string(sequence.fetch_add(1, std::memory_order_relaxed)); - const auto candidate = - parent / std::filesystem::u8path(base + ".mjbase-stage." + suffix); - if (FileUtil::create_file(candidate.generic_u8string())) - return candidate.generic_u8string(); - } - return {}; -} - -bool publishStagedOutput(std::string& stagedOutput, const CliOptions& options, - CliRunReport& report) -{ - if (FileUtil::move(stagedOutput, options.output, false)) { - stagedOutput.clear(); - return true; - } - setFailure(report, CliFailureKind::Write, tr("mbase.cli.runner.err.publish_failed")); - report.failed = report.processed; - report.processed = 0; - std::fprintf(stderr, "Error: failed to publish %s\n", options.output.c_str()); - return false; -} - -InputKind detectInputKind(const std::string& path) -{ - const auto view = ca::str::Utf8StringRef::from_data( - reinterpret_cast(path.data()), path.size()); - if (mjbase::jar::JarUtil::isJarFile(view)) return InputKind::Jar; - if (mjbase::jar::JarUtil::isClassFile(view)) return InputKind::Class; - return InputKind::Unsupported; -} - -// uri 形如 "org/example/Demo.class",targetClass 形如 "org/example/Demo"。 -// 子串匹配(去 .class 后缀更稳),空 target 表示不过滤(返回 true)。 -bool classUriMatches(std::string_view uri, const std::string& targetClass) -{ - if (targetClass.empty()) return true; - std::string_view base = uri; - constexpr std::string_view suffix = ".class"; - if (base.size() > suffix.size() && - base.substr(base.size() - suffix.size()) == suffix) { - base.remove_suffix(suffix.size()); - } - return base.find(targetClass) != std::string_view::npos; -} - -model::ModelResult importInput(model::ResourceContainer& container, InputKind kind, - const std::string& path) -{ - if (kind == InputKind::Jar) return model::ResourceHelper::importFromJar(container, path); - return model::ResourceHelper::importFromClassFile(container, path); -} - -model::ModelResult exportOutput(model::ResourceContainer& container, InputKind kind, - const std::string& path, - const std::string& singleClassUri) -{ - if (kind == InputKind::Jar) return model::ResourceHelper::exportToJar(container, path); - return model::ResourceHelper::exportToClassFile(container, path, singleClassUri); -} - -} // namespace - -CliRunReport runCli(const CliOptions& options, const ClassProcessor& processor) -{ - CliRunReport report; - - if (options.input.empty() || options.output.empty()) { - setFailure(report, CliFailureKind::InvalidArguments, tr("mbase.cli.runner.err.empty_path")); - return report; // 调用方负责校验必填项并报错 - } - if (!validateOutputTarget(options, report)) return report; - - model::ResourceContainer container; - - const auto kind = detectInputKind(options.input); - if (kind == InputKind::Unsupported) { - setFailure(report, CliFailureKind::UnsupportedInput, - tr("mbase.cli.runner.err.unsupported_input")); - std::fprintf(stderr, - "Error: unsupported input type for %s (expected .jar or .class)\n", - options.input.c_str()); - return report; // 不支持的输入类型 - } - auto imported = importInput(container, kind, options.input); - if (imported.is_err()) { - setFailure(report, CliFailureKind::Read, - std::string("import failed: ") + - model::to_string(std::move(imported).unwrap_err())); - std::fprintf(stderr, "Error: failed to read %s: %s\n", options.input.c_str(), - report.failureMessage.c_str()); - return report; - } - - // 遍历所有 ClassResource,对每个跑 processor - auto& resources = container.getResources(); - std::string singleClassUri; - for (auto& [uri, resPtr] : resources) { - model::IResource* raw = resPtr.get(); - if (raw->getType() != model::ResourceType::Class) continue; - // -c 过滤:只处理匹配 targetClass 的类 - if (!classUriMatches(uri, options.targetClass)) continue; - auto* cls = ca::core::tag_cast::cast(raw); - - auto bytes = cls->get(); - auto result = processor(uri, bytes); - if (result.ok) { - cls->set(std::move(bytes)); - report.processed++; - } - else { - setFailure(report, CliFailureKind::Processing, result.message); - std::fprintf(stderr, " FAIL: %s - %s\n", std::string(uri).c_str(), - result.message.c_str()); - report.failed++; - } - singleClassUri = std::string(uri); - } - - if (report.processed == 0 && report.failed == 0) { - if (!options.targetClass.empty()) { - setFailure(report, CliFailureKind::NoMatch, tr("mbase.cli.runner.err.no_class_match")); - std::fprintf(stderr, - "Error: no class files matched --class '%s' in %s\n", - options.targetClass.c_str(), - options.input.c_str()); - } - else { - setFailure(report, CliFailureKind::NoClass, tr("mbase.cli.runner.err.no_class")); - std::fprintf(stderr, "Error: no class files found in %s\n", options.input.c_str()); - } - return report; - } - if (report.failed != 0) return report; - - std::string stagedOutput = createStagedOutput(options.output); - if (stagedOutput.empty()) { - setFailure(report, CliFailureKind::Write, tr("mbase.cli.runner.err.temp_failed")); - report.failed = report.processed; - report.processed = 0; - return report; - } - - // 写出 - auto exported = exportOutput(container, kind, stagedOutput, singleClassUri); - if (exported.is_err()) { - FileUtil::remove(stagedOutput); - std::fprintf(stderr, "Error: failed to write %s: %s\n", options.output.c_str(), - model::to_string(exported.unwrap_err())); - setFailure(report, CliFailureKind::Write, - std::string("export failed: ") + model::to_string(exported.unwrap_err())); - report.failed = report.processed; // 全部视为失败 - report.processed = 0; - return report; - } - - if (!verifyOutput(stagedOutput, kind)) { - FileUtil::remove(stagedOutput); - setFailure(report, CliFailureKind::Verification, tr("mbase.cli.runner.err.verify_failed")); - report.failed = report.processed; - report.processed = 0; - return report; - } - if (!publishStagedOutput(stagedOutput, options, report)) { - FileUtil::remove(stagedOutput); - return report; - } - - std::printf("Done: %d classes processed, %d failed -> %s\n", - report.processed, - report.failed, - options.output.c_str()); - return report; -} - -} // namespace mjbase::cli diff --git a/mjbase/src/mjbase/cli/cli_runner.hpp b/mjbase/src/mjbase/cli/cli_runner.hpp deleted file mode 100644 index b167e00..0000000 --- a/mjbase/src/mjbase/cli/cli_runner.hpp +++ /dev/null @@ -1,143 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "libca/core/datatype.hpp" - -namespace mjbase::cli { - -/// @brief CLI 通用参数(-i / -o / --jdk / -c / -m)。 -/// -/// input/output 为必填,jdkPath 为空表示不启用 COMPUTE_FRAMES。 -/// -/// targetClass(-c)非空时只限制处理集合:匹配类会被处理, -/// 未匹配 class 和非 class 资源仍会保留并写入输出 JAR。targetMethod(-m) -/// 非空时只处理匹配方法,类本身仍参与读写 roundtrip。 -struct CliOptions -{ - std::string input; - std::string output; - std::string jdkPath; - std::string targetClass; // -c: internal name 子串匹配(org/example/Demo) - std::string targetMethod; // -m: "name" 或 "name()I" - bool quiet = false; // --quiet: 完全静默,仅最终总结行 - bool verbose = false; // --verbose: 非 TTY 下也输出换行式阶段日志 - // keep 配置(审计 P0#1:keep 规则的 CLI 暴露面)。runCli 不消费——它们服务 - // 于跑全局收缩管线的命令(规则文件经消费方既有 IO 路径读入;档位文本经 - // mjbase::cli::keepTierOf 解析成 reference::KeepTier)。 - std::string keepFile; // --keep : keep 规则 DSL 文件 - std::string keepTier; // --keep-tier : none|public|public-protected -}; - -/// @brief 单个 class 的处理结果。 -struct ClassProcessResult -{ - bool ok = true; - std::string message; -}; - -/// @brief class 字节处理器:输入 URI + class 字节,原地修改字节并返回处理状态。 -/// -/// @param uri 资源 URI(jar 内路径或 class 文件路径,借用视图,回调期内有效) -/// @param bytes class 字节(引用,处理器直接原地改写) -/// @return 处理结果(ok=false 时计入失败计数,message 用于日志) -using ClassProcessor = - std::function& bytes)>; - -/// CLI 运行失败的稳定分类,供命令行和发布 smoke test 使用。 -enum class CliFailureKind -{ - None, - InvalidArguments, - UnsupportedInput, - Read, - NoClass, - NoMatch, - Processing, - Write, - Verification, -}; - -/// @brief 整体运行报告。 -/// -/// `failure` 和 `failureMessage` 是稳定的机器可读失败契约;调用方不得只依据 stderr -/// 文本判断失败原因。成功写出后 runner 会重新读取输出,并确认每个 class 都能重新解析; -/// JAR 中后缀为 `.class` 但未被识别为 class 资源的条目同样会导致 `Verification`。 -struct CliRunReport -{ - ca::i32 processed = 0; // 成功处理的 class 数 - ca::i32 failed = 0; // 失败的 class 数 - CliFailureKind failure = CliFailureKind::None; - std::string failureMessage; - // 成功 = 处理了至少一个 class 且无失败(空输入/不支持的类型/全部失败都算失败) - bool ok() const { return processed > 0 && failed == 0; } -}; - -/// @brief CLI 公共骨架:import → 对每个 ClassResource 跑 processor → export。 -/// -/// 消除各工具重复的「解析参数 → 读资源 → 转换 → 写出」流程。文件 IO 走 -/// ca::fs::FileUtil / ResourceHelper。输出先写入同目录临时文件,逐个重新解析 -/// class 验证后才发布到此前不存在的目标路径。 -/// -/// 输入类型由 JarUtil::isJarFile / isClassFile 自动分发: -/// - jar:importFromJar → 遍历所有 ClassResource → exportToJar -/// - class:importFromClassFile → 处理 → exportToClassFile -/// -/// @param options CLI 参数 -/// @param processor class 字节处理器 -/// @return 运行报告;输入非法(空路径/不支持的类型/无 class)时 report.ok()=false -/// 且 processed=0,调用方据此返回非零退出码。 -CliRunReport runCli(const CliOptions& options, const ClassProcessor& processor); - -/// @brief 把 targetMethod("name" 或 "name()I")解析成方法过滤器。 -/// -/// - "name" → name 精确匹配,descriptor 任意 -/// - "name()I" → name 精确匹配 AND descriptor 精确匹配 -/// - "" → 返回 nullopt(不过滤) -/// -/// buildMethodPredicate 的过滤契约落在 (name, descriptor) 借用视图上, -/// 消费方把成员元数据投到视图(匹配规则不变)。 -class MethodFilter -{ -public: - /// @brief 解析 targetMethod 文本;空串返回 nullopt(不过滤)。 - static std::optional parse(const std::string& targetMethod) - { - if (targetMethod.empty()) return std::nullopt; - - MethodFilter filter; - // 拆 "name(desc)":左括号前是 name,括号内(含到结尾)是 desc - auto paren = targetMethod.find('('); - if (paren == std::string::npos) { - filter.name_ = targetMethod; // 纯名字,desc 任意 - } - else { - filter.name_ = targetMethod.substr(0, paren); - filter.desc_ = targetMethod.substr(paren); // 含 '(' - } - return filter; - } - - /// @brief 成员是否命中:name 精确匹配;desc 规格非空时还需精确匹配。 - bool matches(std::string_view name, std::string_view desc) const - { - if (name != std::string_view(name_)) return false; // name 精确匹配 - if (!desc_.empty() && desc != std::string_view(desc_)) return false; - return true; - } - - /// @brief 注册时的名字规格(诊断/展示用)。 - const std::string& name() const { return name_; } - /// @brief 注册时的描述符规格(含 '(',空表示不限定)。 - const std::string& descriptor() const { return desc_; } - -private: - std::string name_; ///< 方法名规格 - std::string desc_; ///< 描述符规格("" = 任意) -}; - -} // namespace mjbase::cli diff --git a/mjbase/src/mjbase/cli/i18n_tables.hpp b/mjbase/src/mjbase/cli/i18n_tables.hpp deleted file mode 100644 index bdd06ab..0000000 --- a/mjbase/src/mjbase/cli/i18n_tables.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -// mjbase/cli 框架文本翻译表的接入入口。 -// -// 各产品线 main 在 ca::i18n::init 时并入本表(ArgParser 的 help/错误文案即随 -// 语言本地化): -// -// #include "mjbase/cli/i18n_tables.hpp" -// #include "i18n__translations.generated.hpp" // 线主表 -// ca::i18n::init({mjbase::cli::kCliI18nTableSet, kI18nTableSet_}, -// {"_LANG"}); -// -// 为什么不做静态自注册:注册器所在 obj 无被引用符号,链接器会整段丢弃 -// (MSVC 按需拉取),不可靠;显式 init 是 libi18n 的既有约定。 -// -// 生成头 i18n_mjbase_translations.generated.hpp 位于 mjbase target 的 -// autogendir(rule libca.i18n.embed-lang 以 public includedir 暴露),仅依赖 -// mjbase 的 target 可见。key 空间沿用 mbase 单源表的 mbase.cli.* 前缀 -// (tr() 按字面 key 查表);后注册的产品线主表可覆盖同 key。 - -#include "i18n_mjbase_translations.generated.hpp" -#include "libca/i18n/i18n.hpp" - -namespace mjbase::cli { - -/// mjbase 框架文本表(mbase.cli.* key,文案自 mbase 单源表原样嵌入); -/// 后注册的产品线主表可覆盖同 key。 -inline constexpr ca::i18n::TableSet kCliI18nTableSet = kI18nTableSet_mjbase; - -} // namespace mjbase::cli diff --git a/mjbase/src/mjbase/cli/keep_cli.hpp b/mjbase/src/mjbase/cli/keep_cli.hpp deleted file mode 100644 index c7cf854..0000000 --- a/mjbase/src/mjbase/cli/keep_cli.hpp +++ /dev/null @@ -1,79 +0,0 @@ -/// @file keep_cli.hpp -/// @brief keep 配置的命令行暴露面(审计 P0#1)。 -/// -/// 三条纪律: -/// -/// 1. **只做搬运**:本面把命令行文本搬进 `CliOptions`,再把档位文本解析成 -/// `reference::KeepTier`(经 `reference::keepTierFrom`)。规则的**读入**留 -/// 给命令侧既有 IO 路径(`--keep ` 是路径,不是规则内容)——避免 -/// CLI 层替消费方决定文件编码与错误呈现。 -/// 2. **fail-loud 不降级**:未知档位名 `Err(UnknownTier)`。命令行写错档位比 -/// 「静默按 none 处理」安全得多(后者会让库型 JAR 的 protected 面被删)。 -/// 3. **帮助文本不本地化**:本面提供缺省 help 原文;需要本地化的命令可在 -/// 注册后用 `ArgParser::add` 覆盖同名 spec(canonical 名见下),或自行 -/// 渲染 `tr` 后传入。 -/// -/// canonical 名(消费方据以取值):`--keep`、`--keep-tier`。 -#pragma once - -#include -#include - -#include "libca/core/result.hpp" - -#include "mjbase/cli/arg_parser.hpp" -#include "mjbase/cli/cli_runner.hpp" -#include "mjbase/reference/keep_rule.hpp" - -namespace mjbase::cli { - -// ── canonical option 名(解析取值与覆盖注册共用)── -inline constexpr std::string_view kOptKeep = "--keep"; -inline constexpr std::string_view kOptKeepTier = "--keep-tier"; - -/// @brief 注册 keep 相关 option(命令侧一行接入)。 -/// -/// - `--keep `:keep 规则 DSL 文件路径(String) -/// - `--keep-tier `:`none` / `public` / `public-protected`(String) -/// -/// 两个 option 都非必填;缺失时对应字段留空(= 不叠加该来源)。 -inline void addKeepOptions(ArgParser& parser) -{ - parser.add({"--keep", "-k"}, OptKind::String, "", - "keep 规则 DSL 文件(class/member/preset 行式规则)"); - parser.add({"--keep-tier"}, OptKind::String, "", - "keep 档位:none | public | public-protected"); -} - -/// @brief 从解析结果取 keep 配置写入 `CliOptions`(未出现的项保持原值)。 -/// -/// 只覆盖两个 keep 字段,不触碰 input/output 等——命令可自由组合本面与自有 -/// 注册项。 -inline void applyKeepOptions(const ArgResult& result, CliOptions& options) -{ - if (result.has(std::string(kOptKeep))) { - options.keepFile = result.getString(std::string(kOptKeep), options.keepFile); - } - if (result.has(std::string(kOptKeepTier))) { - options.keepTier = result.getString(std::string(kOptKeepTier), options.keepTier); - } -} - -/// @brief 档位文本 → `reference::KeepTier`(空串 = None)。 -/// -/// 未知档位 `Err(KeepRuleErrorCode::UnknownTier)`:命令行档位写错必须稳定 -/// 失败,不静默降级为 none。 -inline ca::core::Result keepTierOf( - const CliOptions& options) -{ - return reference::keepTierFrom(options.keepTier); -} - -/// @brief 档位文本 → `reference::KeepTier`(免 CliOptions 的直接形态)。 -inline ca::core::Result keepTierOf( - std::string_view tierText) -{ - return reference::keepTierFrom(tierText); -} - -} // namespace mjbase::cli diff --git a/mjbase/src/mjbase/cli/output_sink.hpp b/mjbase/src/mjbase/cli/output_sink.hpp deleted file mode 100644 index d2bdc10..0000000 --- a/mjbase/src/mjbase/cli/output_sink.hpp +++ /dev/null @@ -1,91 +0,0 @@ -#pragma once - -// ── 输出抽象(FILE* 封装)────────────────────────────────────────── -// -// 替代散落在各 cmd_*.cpp 的 fprintf(stdout/stderr, ...)。默认指向 stdout/stderr -// (行为与现状一致),但可指向任意 FILE*(测试捕获、重定向、换输出后端), -// 让输出可重定向/可测试。 -// -// 用法: -// mjbase::cli::out().printf(" Version: %d.%d\n", major, minor); -// mjbase::cli::err().printf("Error: %s\n", msg); -// mjbase::cli::out().puts("done"); -// -// 测试捕获: -// mjbase::cli::OutputSink sink(tmpfile); // 或注入到命令 -// sink.printf(...); -// -// CLI 输出 sink。 - -#include -#include -#include - -namespace mjbase::cli { - -/// FILE* 封装的输出 sink。printf 签名与 fprintf 一致(首参为 sink 自身), -/// 故 fprintf(stdout, fmt, ...) → out().printf(fmt, ...) 是 1:1 机械替换。 -class OutputSink -{ -public: - explicit OutputSink(FILE* f) - : file_(f) - {} - - /// 变参格式化输出(与 fprintf 同语义,写到 sink 持有的 FILE*)。 - void printf(const char* fmt, ...) const - { - va_list ap; - va_start(ap, fmt); - std::vfprintf(file_, fmt, ap); - va_end(ap); - } - - /// 输出一行(自动加换行)。 - void puts(const std::string& s) const - { - std::fputs(s.c_str(), file_); - std::fputc('\n', file_); - } - - /// 输出原始字符串(不加换行)。 - void write(const std::string& s) const { std::fputs(s.c_str(), file_); } - - FILE* file() const { return file_; } - -private: - FILE* file_; -}; - -/// 默认 stdout sink(进程级单例,避免每个 cmd 都构造)。 -inline OutputSink& out() -{ - static OutputSink sink(stdout); - return sink; -} - -/// 默认 stderr sink。 -inline OutputSink& err() -{ - static OutputSink sink(stderr); - return sink; -} - -// ── format 解析(消除 classes/strings/xref/jar list 各手搓的 --format 校验)── - -enum class Format -{ - Plain, - Json, - Csv -}; - -/// 把 "plain"/"json"/"csv" 解析为 Format;非法返回 Plain(调用方按需报错)。 -inline Format parseFormat(const std::string& s) -{ - if (s == "json") return Format::Json; - if (s == "csv") return Format::Csv; - return Format::Plain; -} - -} // namespace mjbase::cli diff --git a/mjbase/src/mjbase/cli/translations/en_US.lang b/mjbase/src/mjbase/cli/translations/en_US.lang deleted file mode 100644 index 917cf4d..0000000 --- a/mjbase/src/mjbase/cli/translations/en_US.lang +++ /dev/null @@ -1,144 +0,0 @@ -# mbase/cli framework texts shared by every line using ArgParser. - -mbase.cli.usage=Usage -mbase.cli.options_header=Options: -mbase.cli.help_flag=Show this help -mbase.cli.err.unknown_option=unknown option '{0}'. -mbase.cli.err.flag_no_value=flag '{0}' takes no value. -mbase.cli.err.requires_argument={0} requires an argument. -mbase.cli.err.empty_argument={0} requires a non-empty argument. -mbase.cli.err.unexpected_argument=unexpected argument '{0}'. -mbase.cli.err.missing_required={0} is required. -mbase.cli.err.mutex_conflict=options in the same group are mutually exclusive. -mbase.cli.err.mutex_required=one of the required options must be given. -mbase.cli.err.invalid_integer={0} expects an integer, got '{1}'. - -# ── pass descriptions (pass_name_registry.cpp) ──────────────── -mbase.opt.pass.arithmetic_simplifier=arithmetic simplification (iinc merging, constant folding, etc.) -mbase.opt.pass.expression_simplifier=expression simplification (redundant dup/pop, etc.) -mbase.opt.pass.stack_operation_simplifier=stack operation simplification (dup_x1 elimination, etc.) -mbase.opt.pass.type_cast_simplifier=type cast simplification (redundant checkcast, etc.) -mbase.opt.pass.fix_pattern_simplifier=fixed pattern simplification (multi-round cleanup) -mbase.opt.pass.nop_remover=remove NOP instructions -mbase.opt.pass.unused_label_remover=remove unreferenced labels -mbase.opt.pass.illegal_access_remover=remove illegal access flags -mbase.opt.pass.meta_info_remover=remove meta info (debug/annotations, etc.) -mbase.opt.pass.invalid_try_catch_remover=remove invalid try-catch blocks -mbase.opt.pass.specific_annotation_remover=remove annotations with the given descriptor -mbase.opt.pass.illegal_class_file_cleaner=clean up malformed classfile structures -mbase.opt.pass.illegal_annotation_remover=remove illegal annotations -mbase.opt.pass.illegal_signature_remover=remove illegal signatures -mbase.opt.pass.illegal_type_annotation_remover=remove illegal type annotations -mbase.opt.pass.illegal_varargs_remover=remove illegal varargs flags -mbase.opt.pass.illegal_source_info_remover=remove illegal source debug info -mbase.opt.pass.unknown_attribute_cleaner=remove unknown attributes -mbase.opt.pass.illegal_local_variable_info_remover=remove illegal local variable name info -mbase.opt.pass.final_remover=remove ACC_FINAL from fields -mbase.opt.pass.kotlin_metadata_remover=remove @kotlin/Metadata and jetbrains annotations -mbase.opt.pass.source_info_remover=clear sourceFile/sourceDebug -mbase.opt.pass.line_number_remover=delete line number nodes in methods -mbase.opt.pass.synthetic_bridge_remover=remove synthetic/bridge flags -mbase.opt.pass.local_variable_info_remover=clear method local variable tables -mbase.opt.pass.parameters_info_remover=clear method parameter info -mbase.opt.pass.exception_info_remover=clear method exception declarations -mbase.opt.pass.long_indy_name_optimizer=shorten overlong indy names -mbase.opt.pass.signature_remover=clear class/method/field signatures -mbase.opt.pass.constant_folder=constant folding -mbase.opt.pass.simple_constant_folder=simple constant folding (SourceInterpreter frame analysis) -mbase.opt.pass.local_constant_folder=local constant folding (sliding window within basic blocks + stack frame verification) -mbase.opt.pass.cons_local_var_optimizer=optimize constant local variables with a single store -mbase.opt.pass.label_cons_local_var_propagator=propagate constant local variables across labels -mbase.opt.pass.cons_return_propagator=constant return value propagation driven by shared pure function summaries -mbase.opt.pass.cons_static_field_replacer=replace constant static fields with constant loads -mbase.opt.pass.constant_switch_folder=fold constant switches -mbase.opt.pass.constant_state_machine_eliminator=eliminate constant state machines -mbase.opt.pass.cons_jump_optimizer=optimize conditional jumps -mbase.opt.pass.redundant_jump_remover=remove redundant jumps -mbase.opt.pass.safe_dead_code_remover=safely remove dead code -mbase.opt.pass.impossible_jump_eliminater=eliminate impossible gotos -mbase.opt.pass.redundant_try_catch_remover=eliminate redundant try-catch blocks -mbase.opt.pass.try_catch_repair=repair try-catch blocks with overlapping/missing labels -mbase.opt.pass.goto_rearranger=rearrange control flow scrambled by gotos -mbase.opt.pass.dead_local_store_remover=remove dead local variable stores -mbase.opt.pass.pop_unused_local_variables=pop unused local variables -mbase.opt.pass.useless_pop_cleaner=remove meaningless pop instructions -mbase.opt.pass.stack_operation_expander=expand constant DUP/SWAP stack operations into explicit constant loads -mbase.opt.pass.redundant_invoke_remover=eliminate redundant cross-class pure function calls -mbase.opt.pass.commutative_operator_reorder=reorder commutative operations -mbase.opt.pass.redundant_load_val_eliminator=eliminate redundant load/val -mbase.opt.pass.no_load_store_eliminater=eliminate stores without loads -mbase.opt.pass.redundant_store_eliminater=replace redundant stores with pop -mbase.opt.pass.arithmetic_insn_rearranger=rearrange consecutive bitwise assignment blocks -mbase.opt.pass.unused_result_call_eliminater=remove useless constant stdlib calls -mbase.opt.pass.unused_check_cleaner=remove useless requireNonNull checks -mbase.opt.pass.char_arr_to_str_simplifier=simplify char[] to String construction -mbase.opt.pass.const_char_array_string_folder=fold constant char array String construction -mbase.opt.pass.box_unbox_eliminator=eliminate boxing/unboxing -mbase.opt.pass.random_seed_resolver=resolve random seeds -mbase.opt.pass.std_cons_function_optimizer=optimize standard constructor calls -mbase.opt.pass.local_constant_propagation=local constant propagation -mbase.opt.pass.continuous_label_merger=merge consecutive LabelNodes -mbase.opt.pass.label_dead_var_store_eliminator=eliminate dead local variable stores across labels -mbase.opt.pass.enum_cast_check_remover=remove redundant checkcast on enum classes -mbase.opt.pass.useless_try_catch_remover=eliminate useless try-catch blocks -mbase.opt.pass.self_caught_throw_rewriter=rewrite self-caught throw jumps into direct jumps (new/dup/init/athrow → goto handler) -mbase.opt.pass.dead_side_effect_free_call_remover=eliminate dead chains where side-effect-free call results (nanoTime/random) are discarded by pop -mbase.opt.pass.unused_method_remover=remove uncalled private static methods in classes -mbase.opt.pass.empty_class_remover=remove empty class resources without fields or methods -mbase.opt.pass.unused_class_remover=remove class resources outside the global reachability closure -mbase.opt.pass.unused_field_remover=remove globally unreachable private fields -mbase.opt.pass.global_unused_method_remover=remove globally unreachable private methods -mbase.opt.pass.static_field_default_inliner=inline static field defaults into ConstantValue -mbase.opt.pass.getter_inliner=conservatively inline simple getters -mbase.opt.pass.short_method_inliner=conservatively inline short constant methods -mbase.opt.pass.arg_forward_method_inliner=conservatively inline argument forwarding methods -mbase.opt.pass.pure_function_annotator=annotate static pure functions -mbase.opt.pass.pure_function_localizer=localize cross-class pure functions -mbase.opt.pass.local_pure_function_inliner=inline in-class pure function calls -mbase.opt.pass.method_inliner=general method body inlining -mbase.opt.pass.global_str_pool_optimizer=global string pool optimization -mbase.opt.pass.tool_class_combiner=merge static utility classes -mbase.opt.pass.class_member_sorter=sort class fields and methods -mbase.opt.pass.stack_trace_recover=recover line numbers/source files/stack traces -mbase.opt.pass.local_variable_info_rebuilder=rebuild local variable info tables -mbase.opt.pass.access_modifier_tightener=tighten access modifiers without external references -mbase.opt.pass.class_renamer=rename classes - -# ── pipeline descriptions (pass_registry.cpp) ───────────────── -mbase.opt.pipeline.phase.fix_pattern_replace=Legacy fixPatternReplaceManager: type cast/commutative reorder/arithmetic/stack/fixed pattern/expression simplification -mbase.opt.pipeline.phase.simple_constant_fold=Legacy simpleConstantFoldManager: constant folding after unused label cleanup -mbase.opt.pipeline.phase.control_flow_simplify=Legacy controlFlowSimplifyManager: constant switch/conditional jump/redundant jump/safe dead code/label cleanup -mbase.opt.pipeline.phase.peephole_garbage_clean=Legacy peepholeGarbageCleanManager: safe dead code/nop/label/unused local variable/pop cleanup -mbase.opt.pipeline.default_optimize=default instruction-level optimization: fix-pattern -> simple-constant-fold -> control-flow -> peephole-clean -mbase.opt.pipeline.phase.meta_info_remove=Legacy allMetaInfoRemoveManager: final/synthetic-bridge/kotlin-metadata/source/line/localvar/param/exception/signature cleanup -mbase.opt.pipeline.phase.illegal_meta_info_remove=Legacy allIllegalMetaInfoRemoveManager: illegal annotation/signature/type annotation/source/varargs cleanup - -# ── cli_runner failure messages (cli_runner.cpp) ────────────── -mbase.cli.runner.err.output_exists=output path already exists -mbase.cli.runner.err.publish_failed=unable to commit the verified output file -mbase.cli.runner.err.empty_path=input or output path is empty -mbase.cli.runner.err.unsupported_input=input is not a jar or class file -mbase.cli.runner.err.no_class_match=no class file matches --class -mbase.cli.runner.err.no_class=no class file in input -mbase.cli.runner.err.temp_failed=unable to create temporary output file -mbase.cli.runner.err.verify_failed=no valid class after re-reading the output -mbase.cli.runner.err.no_resource_or_class=no resource or class file in input - -# ── sir_verifier validation messages (sir_verifier.cpp) ─────── -mbase.sir.verifier.entry_block_oob=entry block index out of bounds -mbase.sir.verifier.param_value_missing=parameter references a nonexistent SSA value -mbase.sir.verifier.block_index_oob=block index out of bounds -mbase.sir.verifier.successor_oob=successor block index out of bounds -mbase.sir.verifier.cfg_mismatch=CFG predecessor/successor relation is asymmetric -mbase.sir.verifier.phi_not_at_start=Phi nodes must be at the start of a block -mbase.sir.verifier.phi_missing_incoming=Phi node is missing its incoming table -mbase.sir.verifier.phi_incoming_count_mismatch=Phi incoming count does not match predecessor count -mbase.sir.verifier.phi_incoming_not_predecessor=Phi incoming is not a predecessor block -mbase.sir.verifier.phi_incoming_value_missing=Phi incoming references a nonexistent SSA value -mbase.sir.verifier.result_index_oob=instruction result SSA value index out of bounds -mbase.sir.verifier.operand_value_missing=instruction references a nonexistent SSA value -mbase.sir.verifier.terminator_not_at_end=terminator instruction must be at the end of a block -mbase.sir.verifier.missing_terminator=block is missing an explicit terminator -mbase.sir.verifier.value_not_defined_once=every SSA value must be defined exactly once -mbase.sir.verifier.call_arguments_reserved=only Call may carry an argument list -mbase.sir.verifier.call_arguments_invalid=Call argument count or argument SSA value is invalid diff --git a/mjbase/src/mjbase/cli/translations/zh_CN.lang b/mjbase/src/mjbase/cli/translations/zh_CN.lang deleted file mode 100644 index fed5aa2..0000000 --- a/mjbase/src/mjbase/cli/translations/zh_CN.lang +++ /dev/null @@ -1,145 +0,0 @@ -# mbase/cli 框架文本(所有用 ArgParser 的产品线共享)。zh_CN 为唯一事实源。 -# 静态注册:i18n_setup.cpp 在 main 前自动 init(各线主表后注册者胜)。 - -mbase.cli.usage=用法 -mbase.cli.options_header=选项: -mbase.cli.help_flag=显示此帮助 -mbase.cli.err.unknown_option=未知选项:'{0}'。 -mbase.cli.err.flag_no_value=开关 '{0}' 不接受值。 -mbase.cli.err.requires_argument={0} 需要一个参数。 -mbase.cli.err.empty_argument={0} 需要非空参数。 -mbase.cli.err.unexpected_argument=意外的参数:'{0}'。 -mbase.cli.err.missing_required={0} 为必填项。 -mbase.cli.err.mutex_conflict=同组选项互斥。 -mbase.cli.err.mutex_required=必填组内至少提供一个选项。 -mbase.cli.err.invalid_integer={0} 需要整数,得到 '{1}'。 - -# ── pass 描述(pass_name_registry.cpp)──────────────────────── -mbase.opt.pass.arithmetic_simplifier=算术运算化简(iinc 合并、常量折叠等) -mbase.opt.pass.expression_simplifier=表达式化简(冗余 dup/pop 等) -mbase.opt.pass.stack_operation_simplifier=栈操作化简(dup_x1 消除等) -mbase.opt.pass.type_cast_simplifier=类型转换化简(冗余 checkcast 等) -mbase.opt.pass.fix_pattern_simplifier=固定模式化简(多轮收尾) -mbase.opt.pass.nop_remover=清除 NOP 指令 -mbase.opt.pass.unused_label_remover=清除未引用的标签 -mbase.opt.pass.illegal_access_remover=移除非法访问标志 -mbase.opt.pass.meta_info_remover=移除元信息(调试/注解等) -mbase.opt.pass.invalid_try_catch_remover=移除无效的 try-catch 块 -mbase.opt.pass.specific_annotation_remover=移除指定描述符的注解 -mbase.opt.pass.illegal_class_file_cleaner=清理畸形 classfile 结构 -mbase.opt.pass.illegal_annotation_remover=移除非法注解 -mbase.opt.pass.illegal_signature_remover=移除非法签名 -mbase.opt.pass.illegal_type_annotation_remover=移除非法类型注解 -mbase.opt.pass.illegal_varargs_remover=移除非法 varargs 标志 -mbase.opt.pass.illegal_source_info_remover=移除非法源码调试信息 -mbase.opt.pass.unknown_attribute_cleaner=移除未知属性 -mbase.opt.pass.illegal_local_variable_info_remover=移除非法局部变量名信息 -mbase.opt.pass.final_remover=移除字段的 ACC_FINAL -mbase.opt.pass.kotlin_metadata_remover=移除 @kotlin/Metadata 及 jetbrains 注解 -mbase.opt.pass.source_info_remover=清除 sourceFile/sourceDebug -mbase.opt.pass.line_number_remover=删除方法内行号节点 -mbase.opt.pass.synthetic_bridge_remover=移除 synthetic/bridge 标志 -mbase.opt.pass.local_variable_info_remover=清除方法局部变量表 -mbase.opt.pass.parameters_info_remover=清除方法参数信息 -mbase.opt.pass.exception_info_remover=清除方法异常声明 -mbase.opt.pass.long_indy_name_optimizer=缩减超长 indy 名 -mbase.opt.pass.signature_remover=清除类/方法/字段签名 -mbase.opt.pass.constant_folder=常量折叠 -mbase.opt.pass.simple_constant_folder=简单常量折叠(SourceInterpreter 帧分析) -mbase.opt.pass.local_constant_folder=局部常量折叠(基本块内滑动窗口+栈帧验证) -mbase.opt.pass.cons_local_var_optimizer=单次 store 的常量局部变量优化 -mbase.opt.pass.label_cons_local_var_propagator=标签间常量局部变量传播 -mbase.opt.pass.cons_return_propagator=共享纯函数摘要驱动的常量返回值传播 -mbase.opt.pass.cons_static_field_replacer=常量 static 字段替换为常量加载 -mbase.opt.pass.constant_switch_folder=常量 switch 折叠 -mbase.opt.pass.constant_state_machine_eliminator=常量状态机消除 -mbase.opt.pass.cons_jump_optimizer=条件跳转优化 -mbase.opt.pass.redundant_jump_remover=移除冗余跳转 -mbase.opt.pass.safe_dead_code_remover=安全移除死代码 -mbase.opt.pass.impossible_jump_eliminater=不可能的goto消除器 -mbase.opt.pass.redundant_try_catch_remover=无效TryCatch消除优化 -mbase.opt.pass.try_catch_repair=修复重叠/缺失label的trycatch -mbase.opt.pass.goto_rearranger=重排goto指令带来的混乱控制流 -mbase.opt.pass.dead_local_store_remover=移除死局部变量存储 -mbase.opt.pass.pop_unused_local_variables=弹出未使用的局部变量 -mbase.opt.pass.useless_pop_cleaner=清除无意义的 pop 指令 -mbase.opt.pass.stack_operation_expander=将常量 DUP/SWAP 栈操作展开为显式常量加载 -mbase.opt.pass.redundant_invoke_remover=跨类纯函数冗余调用消除 -mbase.opt.pass.commutative_operator_reorder=交换律运算重排 -mbase.opt.pass.redundant_load_val_eliminator=消除冗余 load/val -mbase.opt.pass.no_load_store_eliminater=消除没有 load 的 store -mbase.opt.pass.redundant_store_eliminater=冗余 store 替换为 pop -mbase.opt.pass.arithmetic_insn_rearranger=重排连续位运算赋值块 -mbase.opt.pass.unused_result_call_eliminater=移除常量标准库无效调用 -mbase.opt.pass.unused_check_cleaner=清除无用的 requireNonNull 检查 -mbase.opt.pass.char_arr_to_str_simplifier=char[] 到 String 的化简 -mbase.opt.pass.const_char_array_string_folder=常量 char 数组 String 构造折叠 -mbase.opt.pass.box_unbox_eliminator=装箱/拆箱消除 -mbase.opt.pass.random_seed_resolver=随机种子解析 -mbase.opt.pass.std_cons_function_optimizer=标准构造函数优化 -mbase.opt.pass.local_constant_propagation=局部常量传播 -mbase.opt.pass.continuous_label_merger=合并连续的 LabelNode -mbase.opt.pass.label_dead_var_store_eliminator=标签间无效局部变量存储消除 -mbase.opt.pass.enum_cast_check_remover=移除枚举类多余的 checkcast -mbase.opt.pass.useless_try_catch_remover=无效 Try-Catch 块消除 -mbase.opt.pass.self_caught_throw_rewriter=自捕获异常跳转改直接跳转(new/dup/init/athrow→goto handler) -mbase.opt.pass.dead_side_effect_free_call_remover=消除无副作用调用(nanoTime/random)返回值被 pop 丢弃的死代码链 -mbase.opt.pass.unused_method_remover=移除类内未调用的 private static 方法 -mbase.opt.pass.empty_class_remover=移除无字段无方法的空类资源 -mbase.opt.pass.unused_class_remover=移除全局可达性闭包外的 class 资源 -mbase.opt.pass.unused_field_remover=移除全局不可达的 private 字段 -mbase.opt.pass.global_unused_method_remover=移除全局不可达的 private 方法 -mbase.opt.pass.static_field_default_inliner=static 字段默认值内联到 ConstantValue -mbase.opt.pass.getter_inliner=保守内联简单 getter -mbase.opt.pass.short_method_inliner=保守内联短常量方法 -mbase.opt.pass.arg_forward_method_inliner=保守内联参数转发方法 -mbase.opt.pass.pure_function_annotator=标注 static 纯函数 -mbase.opt.pass.pure_function_localizer=跨类纯函数本地化 -mbase.opt.pass.local_pure_function_inliner=类内纯函数调用内联 -mbase.opt.pass.method_inliner=通用方法体内联 -mbase.opt.pass.global_str_pool_optimizer=全局字符串池化优化 -mbase.opt.pass.tool_class_combiner=合并静态工具类 -mbase.opt.pass.class_member_sorter=对类的字段和方法排序 -mbase.opt.pass.stack_trace_recover=恢复行号/源文件/堆栈跟踪 -mbase.opt.pass.local_variable_info_rebuilder=重建局部变量信息表 -mbase.opt.pass.access_modifier_tightener=收紧无外部引用的访问修饰符 -mbase.opt.pass.class_renamer=类重命名 - -# ── pipeline 描述(pass_registry.cpp)───────────────────────── -mbase.opt.pipeline.phase.fix_pattern_replace=Legacy fixPatternReplaceManager:类型转换/交换律重排/算术/栈/固定模式/表达式化简 -mbase.opt.pipeline.phase.simple_constant_fold=Legacy simpleConstantFoldManager:清理未用标签后执行常量折叠 -mbase.opt.pipeline.phase.control_flow_simplify=Legacy controlFlowSimplifyManager:常量 switch/条件跳转/冗余跳转/安全死代码/标签清理 -mbase.opt.pipeline.phase.peephole_garbage_clean=Legacy peepholeGarbageCleanManager:安全死代码/nop/标签/未用局部变量/pop 清理 -mbase.opt.pipeline.default_optimize=默认指令级优化:fix-pattern -> simple-constant-fold -> control-flow -> peephole-clean -mbase.opt.pipeline.phase.meta_info_remove=Legacy allMetaInfoRemoveManager:final/synthetic-bridge/kotlin-metadata/source/line/localvar/param/exception/signature 清理 -mbase.opt.pipeline.phase.illegal_meta_info_remove=Legacy allIllegalMetaInfoRemoveManager:非法注解/签名/type annotation/source/varargs 清理 - -# ── cli_runner 失败消息(cli_runner.cpp)────────────────────── -mbase.cli.runner.err.output_exists=输出路径已经存在 -mbase.cli.runner.err.publish_failed=无法提交已验证的输出文件 -mbase.cli.runner.err.empty_path=输入或输出路径为空 -mbase.cli.runner.err.unsupported_input=输入不是 jar 或 class 文件 -mbase.cli.runner.err.no_class_match=没有匹配 --class 的 class 文件 -mbase.cli.runner.err.no_class=输入中没有 class 文件 -mbase.cli.runner.err.temp_failed=无法创建临时输出文件 -mbase.cli.runner.err.verify_failed=输出重新读取后没有有效 class -mbase.cli.runner.err.no_resource_or_class=输入中没有资源或 class 文件 - -# ── sir_verifier 校验消息(sir_verifier.cpp)────────────────── -mbase.sir.verifier.entry_block_oob=入口基本块索引越界 -mbase.sir.verifier.param_value_missing=参数引用了不存在的 SSA value -mbase.sir.verifier.block_index_oob=基本块索引越界 -mbase.sir.verifier.successor_oob=后继基本块索引越界 -mbase.sir.verifier.cfg_mismatch=CFG 前驱和后继关系不对称 -mbase.sir.verifier.phi_not_at_start=Phi 节点必须位于基本块开头 -mbase.sir.verifier.phi_missing_incoming=Phi 节点缺少 incoming 侧表 -mbase.sir.verifier.phi_incoming_count_mismatch=Phi incoming 数量和前驱数量不一致 -mbase.sir.verifier.phi_incoming_not_predecessor=Phi incoming 不是前驱基本块 -mbase.sir.verifier.phi_incoming_value_missing=Phi incoming 引用了不存在的 SSA value -mbase.sir.verifier.result_index_oob=指令结果 SSA value 索引越界 -mbase.sir.verifier.operand_value_missing=指令引用了不存在的 SSA value -mbase.sir.verifier.terminator_not_at_end=终结指令必须位于基本块末尾 -mbase.sir.verifier.missing_terminator=基本块缺少显式终结指令 -mbase.sir.verifier.value_not_defined_once=每个 SSA value 必须恰好定义一次 -mbase.sir.verifier.call_arguments_reserved=只有 Call 可以携带实参列表 -mbase.sir.verifier.call_arguments_invalid=Call 实参数量或实参 SSA value 非法 diff --git a/mjbase/src/mjbase/i18n.hpp b/mjbase/src/mjbase/i18n.hpp deleted file mode 100644 index 3c5a6d4..0000000 --- a/mjbase/src/mjbase/i18n.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -// mjbase 全局 i18n 兜底入口。 -// -// 各产品线 main(mjt/mj2x/mjdbg…)通常在启动时调用 ca::i18n::init 注册各自的 -// 翻译表;但 mjbase 的静态库代码可能被 mjdbg、插件宿主等“未显式 init i18n” -// 的环境消费。若这些层直接调用 ca::i18n::tr,未注册 key 会原样返回(裸 key -// 泄漏到用户界面)。 -// -// 本头提供 ensureI18nBoot():首次调用时用 mjbase 自带表兜底注册,保证描述/ -// 错误消息始终可解析(默认 zh_CN)。key 空间与 mbase 保持一致(mbase.* 全部 -// key,文案自 mbase 单源表 cli/translations 原样带入)——tr() 按字面 key 查 -// 表,key 前缀不改。已 init 的宿主不受影响——init 后注册的表格仍按 -// “后注册者胜”覆盖 mjbase 默认文案。 - -#include "i18n_mjbase_translations.generated.hpp" -#include "libca/i18n/i18n.hpp" - -namespace mjbase { - -/// 若宿主未注册任何翻译表,用 mjbase 自带表兜底注册(不覆盖已 init 的宿主)。 -inline void ensureI18nBoot() -{ - if (!ca::i18n::initialized()) { - ca::i18n::init({kI18nTableSet_mjbase}, {}); - } -} - -/// mjbase 文案查询:先确保兜底注册,再按当前语言查询。 -inline const char* tr(const char* key) -{ - ensureI18nBoot(); - return ca::i18n::tr(key); -} - -} // namespace mjbase diff --git a/mjbase/test/unittest/cli/arg_parser_test.cpp b/mjbase/test/unittest/cli/arg_parser_test.cpp deleted file mode 100644 index 01541f1..0000000 --- a/mjbase/test/unittest/cli/arg_parser_test.cpp +++ /dev/null @@ -1,286 +0,0 @@ -#include - -#include -#include - -#include "mjbase/cli/arg_parser.hpp" - -#include "libca/i18n/i18n.hpp" -#include "mjbase/cli/i18n_tables.hpp" - -using namespace mjbase::cli; - -namespace { - -// 辅助:构造 char** argv 并解析。返回解析结果(可能为空 = 错误或 -h)。 -std::optional parseArgs(const ArgParser& p, std::vector args) -{ - std::vector argv; - argv.push_back(const_cast("test")); - std::vector owned = std::move(args); - std::vector keep; - for (auto& s : owned) keep.push_back(&s); - for (auto* s : keep) argv.push_back(s->data()); - return p.parse(static_cast(argv.size()), argv.data()); -} - -ArgParseOutcome parseDetailedArgs(const ArgParser& p, std::vector args) -{ - std::vector argv; - argv.push_back(const_cast("test")); - std::vector owned = std::move(args); - std::vector keep; - for (auto& s : owned) keep.push_back(&s); - for (auto* s : keep) argv.push_back(s->data()); - return p.parseDetailed(static_cast(argv.size()), argv.data()); -} - -ArgParser makeSampleParser() -{ - ArgParser p("test -i -o [pattern]", "sample parser"); - p.add({"--input", "-i"}, OptKind::String, "", "input file", true) - .add({"--output", "-o"}, OptKind::String, "", "output file", true) - .add({"--verbose", "-v"}, OptKind::Flag, "", "verbose") - .add({"--pass", "-p"}, OptKind::CommaList, "

", "passes") - .add({"--length"}, OptKind::Int, "", "length") - .add({"pattern"}, OptKind::Positional, "", "search pattern"); - return p; -} - -} // namespace - -// ── 基本 String/Flag/Positional ───────────────────────────────── -TEST(MjArgParserTest, BasicStringAndFlag) -{ - auto p = makeSampleParser(); - auto r = parseArgs(p, {"-i", "in.jar", "-o", "out.jar", "-v", "mypat"}); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(r->getString("--input"), "in.jar"); - EXPECT_EQ(r->getString("--output"), "out.jar"); - EXPECT_TRUE(r->has("--verbose")); - ASSERT_EQ(r->positionals().size(), 1u); - EXPECT_EQ(r->positionals()[0], "mypat"); -} - -// ── --name=value 等号形式(issue 原文 -p=A,B,C)───────────────── -TEST(MjArgParserTest, EqualsForm) -{ - auto p = makeSampleParser(); - auto r = parseArgs(p, {"-i=in.jar", "-o=out.jar", "--pass=A,B,C"}); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(r->getString("--input"), "in.jar"); - EXPECT_EQ(r->getString("--output"), "out.jar"); - auto list = r->getList("--pass"); - ASSERT_EQ(list.size(), 3u); - EXPECT_EQ(list[0], "A"); - EXPECT_EQ(list[1], "B"); - EXPECT_EQ(list[2], "C"); -} - -// ── 长短名别名都能用 ──────────────────────────────────────────── -TEST(MjArgParserTest, Aliases) -{ - auto p = makeSampleParser(); - auto r = parseArgs(p, {"--input", "a", "--output", "b"}); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(r->getString("--input"), "a"); - - auto p2 = makeSampleParser(); - auto r2 = parseArgs(p2, {"-i", "a", "-o", "b"}); - ASSERT_TRUE(r2.has_value()); - EXPECT_EQ(r2->getString("--input"), "a"); -} - -// ── CommaList 多次出现累加 ────────────────────────────────────── -TEST(MjArgParserTest, CommaListAccumulates) -{ - auto p = makeSampleParser(); - auto r = parseArgs(p, {"-i", "a", "-o", "b", "-p", "A,B", "-p", "C"}); - ASSERT_TRUE(r.has_value()); - auto list = r->getList("--pass"); - ASSERT_EQ(list.size(), 3u); - EXPECT_EQ(list[0], "A"); - EXPECT_EQ(list[2], "C"); -} - -// ── Int 类型 ──────────────────────────────────────────────────── -TEST(MjArgParserTest, IntValue) -{ - auto p = makeSampleParser(); - auto r = parseArgs(p, {"-i", "a", "-o", "b", "--length", "42"}); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(r->getInt("--length"), 42); -} - -TEST(MjArgParserTest, IntDefault) -{ - auto p = makeSampleParser(); - auto r = parseArgs(p, {"-i", "a", "-o", "b"}); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(r->getInt("--length", 99), 99); -} - -// ── 缺必填 → nullopt ──────────────────────────────────────────── -TEST(MjArgParserTest, MissingRequired) -{ - auto p = makeSampleParser(); - auto r = parseArgs(p, {"-i", "a"}); // 缺 -o - EXPECT_FALSE(r.has_value()); -} - -// ── 未知 option → nullopt ─────────────────────────────────────── -TEST(MjArgParserTest, UnknownOption) -{ - auto p = makeSampleParser(); - auto r = parseArgs(p, {"-i", "a", "-o", "b", "--bogus"}); - EXPECT_FALSE(r.has_value()); -} - -// ── -h → nullopt(已打印 help)────────────────────────────────── -TEST(MjArgParserTest, HelpReturnsNullopt) -{ - auto p = makeSampleParser(); - auto r = parseArgs(p, {"-h"}); - EXPECT_FALSE(r.has_value()); -} - -// ── Flag 不吃 =value ──────────────────────────────────────────── -TEST(MjArgParserTest, FlagRejectsValue) -{ - auto p = makeSampleParser(); - auto r = parseArgs(p, {"-i", "a", "-o", "b", "-v=true"}); - EXPECT_FALSE(r.has_value()); -} - -// ── 多个位置参数 ──────────────────────────────────────────────── -TEST(MjArgParserTest, MultiplePositionals) -{ - auto p = makeSampleParser(); - auto r = parseArgs(p, {"-i", "a", "-o", "b", "pat1", "pat2"}); - ASSERT_TRUE(r.has_value()); - ASSERT_EQ(r->positionals().size(), 2u); - EXPECT_EQ(r->positionals()[0], "pat1"); - EXPECT_EQ(r->positionals()[1], "pat2"); -} - -// ── 位置参数但无 Positional spec → 报错 ───────────────────────── -TEST(MjArgParserTest, UnexpectedPositional) -{ - ArgParser p("test -i ", "no positional"); - p.add({"--input", "-i"}, OptKind::String, "", "input", true); - auto r = parseArgs(p, {"-i", "a", "stray"}); - EXPECT_FALSE(r.has_value()); -} - -// ── 非 Int 值报错 ─────────────────────────────────────────────── -TEST(MjArgParserTest, InvalidInt) -{ - auto p = makeSampleParser(); - auto r = parseArgs(p, {"-i", "a", "-o", "b", "--length", "xyz"}); - EXPECT_FALSE(r.has_value()); -} - -TEST(MjArgParserTest, DetailedOutcomeClassifiesUsageFailures) -{ - auto parser = makeSampleParser(); - auto missing = parseDetailedArgs(parser, {"-i", "a", "-o"}); - EXPECT_FALSE(missing.ok()); - EXPECT_EQ(missing.error(), ArgParseError::MissingValue); - - auto empty = parseDetailedArgs(parser, {"-i", "", "-o", "b"}); - EXPECT_FALSE(empty.ok()); - EXPECT_EQ(empty.error(), ArgParseError::EmptyValue); - - auto unknown = parseDetailedArgs(parser, {"-i", "a", "-o", "b", "--bogus"}); - EXPECT_FALSE(unknown.ok()); - EXPECT_EQ(unknown.error(), ArgParseError::UnknownOption); - - auto invalidInteger = parseDetailedArgs(parser, {"-i", "a", "-o", "b", "--length", "x"}); - EXPECT_FALSE(invalidInteger.ok()); - EXPECT_EQ(invalidInteger.error(), ArgParseError::InvalidInteger); - - auto help = parseDetailedArgs(parser, {"--help"}); - EXPECT_FALSE(help.ok()); - EXPECT_TRUE(help.helpRequested()); - EXPECT_EQ(help.error(), ArgParseError::HelpRequested); -} - -// ── 互斥组:组内最多一个 ─────────────────────────────────────── -namespace { -ArgParser makeMutexParser() -{ - ArgParser p("test -i (-s | --interface )", "mutex"); - p.add({"--input", "-i"}, OptKind::String, "", "input", true) - .add({"--super", "-s"}, OptKind::String, "", "super class") - .add({"--interface"}, OptKind::String, "", "interface"); - p.addMutexGroup({"--super", "--interface"}, true); - return p; -} -} // namespace - -TEST(MjArgParserTest, MutexGroupSingleChoice) -{ - auto p = makeMutexParser(); - auto r = parseArgs(p, {"-i", "a", "-s", "Base"}); - ASSERT_TRUE(r.has_value()); - EXPECT_EQ(r->getString("--super"), "Base"); -} - -TEST(MjArgParserTest, MutexGroupViolation) -{ - auto p = makeMutexParser(); - // 组内给了两个 → 报错 - auto r = parseArgs(p, {"-i", "a", "-s", "Base", "--interface", "I"}); - EXPECT_FALSE(r.has_value()); -} - -TEST(MjArgParserTest, DetailedOutcomeClassifiesMutexFailure) -{ - auto parser = makeMutexParser(); - auto outcome = parseDetailedArgs(parser, {"-i", "a", "-s", "Base", "--interface", "I"}); - EXPECT_FALSE(outcome.ok()); - EXPECT_EQ(outcome.error(), ArgParseError::MutexConflict); -} - -TEST(MjArgParserTest, MutexGroupRequiredMissing) -{ - auto p = makeMutexParser(); - // required 组一个都没给 → 报错 - auto r = parseArgs(p, {"-i", "a"}); - EXPECT_FALSE(r.has_value()); -} - -TEST(MjArgParserTest, MutexGroupNotRequired) -{ - // 非 required 组:全不给也行 - ArgParser p("test (-a | -b)", "opt mutex"); - p.add({"--alpha", "-a"}, OptKind::Flag, "", "a").add({"--beta", "-b"}, OptKind::Flag, "", "b"); - p.addMutexGroup({"--alpha", "--beta"}, false); - char* av[] = {const_cast("test")}; - auto res = p.parse(1, av); - ASSERT_TRUE(res.has_value()); // 都不给,非 required,OK -} - -// ── 框架文案 i18n(mbase.cli.* 单源表,显式 init 接入)────────── -TEST(MjArgParserTest, FrameworkTextsLocalize) -{ - // 显式注册 mjbase 框架表(幂等;生产路径由各线 main init)。 - ca::i18n::init({mjbase::cli::kCliI18nTableSet}, {}); - ca::i18n::set_lang("zh_CN"); - - // 默认 zh_CN:框架文案查表命中(而非回退 key 原样)。 - EXPECT_STREQ(ca::i18n::tr("mbase.cli.usage"), "\xE7\x94\xA8\xE6\xB3\x95"); - EXPECT_STRNE(ca::i18n::tr("mbase.cli.err.unknown_option"), "mbase.cli.err.unknown_option"); - - // 切换 en_US:框架文案与占位符消息随之切换。 - ASSERT_TRUE(ca::i18n::set_lang("en_US")); - EXPECT_STREQ(ca::i18n::tr("mbase.cli.usage"), "Usage"); - EXPECT_STREQ(ca::i18n::trf("mbase.cli.err.invalid_integer", {"--n", "xyz"}).c_str(), - "--n expects an integer, got 'xyz'."); - - // zh 占位符消息:{0} 需要整数,得到 '{1}'。 - ASSERT_TRUE(ca::i18n::set_lang("zh_CN")); - EXPECT_STREQ(ca::i18n::trf("mbase.cli.err.invalid_integer", {"--n", "xyz"}).c_str(), - "--n \xE9\x9C\x80\xE8\xA6\x81\xE6\x95\xB4\xE6\x95\xB0\xEF\xBC\x8C\xE5\xBE\x97" - "\xE5\x88\xB0 'xyz'\xE3\x80\x82"); -} diff --git a/mjbase/test/unittest/cli/cli_runner_test.cpp b/mjbase/test/unittest/cli/cli_runner_test.cpp deleted file mode 100644 index a387dba..0000000 --- a/mjbase/test/unittest/cli/cli_runner_test.cpp +++ /dev/null @@ -1,381 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include - -#include "mjbase/cli/cli_runner.hpp" -#include "jbc/core/reader.hpp" -#include "libca/fs/file_util.hpp" -#include "mjbase/model/resource_container.hpp" -#include "mjbase/model/resource_helper.hpp" -#include "mjbase/model/resource_model.hpp" - -using namespace mjbase::cli; -using ca::fs::FileUtil; - -namespace model = mjbase::model; - -namespace { - -// ── 最小可解析 classfile 手工构造 ──────────────────────────────── -// 形态对齐 mbase 测试的 ClassWriter 产物:public class extends -// Object,含一个 public static int t() { return 1; },无 StackMapTable。 -// libjbc 无 tree 式 ClassWriter,字节按骨架直拼(单槽 CP 条目,无 long/double)。 -void putU2(std::vector& out, ca::u16 v) -{ - out.push_back(static_cast(v >> 8)); - out.push_back(static_cast(v & 0xFF)); -} - -void putU4(std::vector& out, ca::u32 v) -{ - putU2(out, static_cast(v >> 16)); - putU2(out, static_cast(v & 0xFFFF)); -} - -std::vector buildMinimalClass(std::string_view className = "T") -{ - std::vector cp; - auto utf8 = [&cp](std::string_view text) { - cp.push_back(0x01); // CP tag:Utf8(单字节) - putU2(cp, static_cast(text.size())); - cp.insert(cp.end(), text.begin(), text.end()); - }; - auto classRef = [&cp](ca::u16 nameIndex) { - cp.push_back(0x07); // CP tag:Class(单字节) - putU2(cp, nameIndex); - }; - // CP #1 Utf8 ,#2 Class #1,#3 Utf8 Object,#4 Class #3, - // #5 Utf8 "t",#6 Utf8 "()I",#7 Utf8 "Code"(全部单槽条目,索引连续)。 - utf8(className); - classRef(1); - utf8("java/lang/Object"); - classRef(3); - utf8("t"); - utf8("()I"); - utf8("Code"); - - std::vector out; - putU4(out, 0xCAFEBABEu); // magic - putU2(out, 0); // minor - putU2(out, 52); // major: V1_8 - putU2(out, 8); // cp_count(#0 不计条目) - out.insert(out.end(), cp.begin(), cp.end()); - putU2(out, 0x0021); // ACC_PUBLIC | ACC_SUPER - putU2(out, 2); // this #2 - putU2(out, 4); // super #4 - putU2(out, 0); // interfaces_count - putU2(out, 0); // fields_count - putU2(out, 1); // methods_count - putU2(out, 0x0009); // ACC_PUBLIC | ACC_STATIC - putU2(out, 5); // name #5 "t" - putU2(out, 6); // descriptor #6 "()I" - putU2(out, 1); // method attributes_count - putU2(out, 7); // attribute name #7 "Code" - putU4(out, 14); // attribute length - putU2(out, 1); // max_stack - putU2(out, 0); // max_locals - putU4(out, 2); // code_length - out.push_back(0x04); // iconst_1 - out.push_back(0xAC); // ireturn - putU2(out, 0); // exception_table_length - putU2(out, 0); // code attributes_count - putU2(out, 0); // class attributes_count - return out; -} - -// RAII 临时文件 -class TempFile -{ -public: - explicit TempFile(std::string suffix) - : path_(std::string(std::tmpnam(nullptr)) + suffix) - {} - ~TempFile() { std::remove(path_.c_str()); } - const std::string& path() const { return path_; } - bool exists() const { return FileUtil::exists(path_); } - -private: - std::string path_; -}; - -} // namespace - -// ── 空参数:返回 ok=false 且 processed=0 ── -TEST(MjCliRunnerTest, emptyOptionsFailsCleanly) -{ - CliOptions opts; - CliRunReport report = - runCli(opts, [](std::string_view, std::vector&) { return ClassProcessResult{}; }); - EXPECT_FALSE(report.ok()); - EXPECT_EQ(report.processed, 0); - EXPECT_EQ(report.failure, CliFailureKind::InvalidArguments); -} - -// ── 不支持的输入类型 ── -TEST(MjCliRunnerTest, unsupportedInputType) -{ - CliOptions opts; - opts.input = "foo.txt"; - opts.output = "bar.txt"; - CliRunReport report = - runCli(opts, [](std::string_view, std::vector&) { return ClassProcessResult{}; }); - EXPECT_FALSE(report.ok()); - EXPECT_EQ(report.processed, 0); - EXPECT_EQ(report.failure, CliFailureKind::UnsupportedInput); -} - -// ── class 文件往返:processor 被调用 + 输出生成 ── -TEST(MjCliRunnerTest, classFileProcessorInvoked) -{ - TempFile in(".class"); - TempFile out(".class"); - - auto bytes = buildMinimalClass(); - ASSERT_TRUE( - FileUtil::write_bytes(in.path(), ca::core::ByteSlice(bytes.data(), bytes.size())).is_ok()); - - int callCount = 0; - std::string seenUri; - ClassProcessor proc = [&](std::string_view uri, std::vector& b) { - callCount++; - seenUri = std::string(uri); - EXPECT_FALSE(b.empty()); - return ClassProcessResult{}; - }; - - CliOptions opts; - opts.input = in.path(); - opts.output = out.path(); - CliRunReport report = runCli(opts, proc); - - EXPECT_EQ(callCount, 1); - EXPECT_EQ(report.processed, 1); - EXPECT_EQ(report.failed, 0); - EXPECT_EQ(report.failure, CliFailureKind::None); - EXPECT_TRUE(report.ok()); - EXPECT_TRUE(out.exists()); - // 输出是合法 class(能被 reader 解析,方法数不变) - auto outBytes = FileUtil::read_all_bytes(out.path()); - ASSERT_TRUE(outBytes.is_ok()); - auto ob = outBytes.unwrap(); - auto read = jbc::core::readClass(jbc::core::ByteView(ob.as_ptr(), ob.len())); - ASSERT_TRUE(read.is_ok()); - auto parsed = std::move(read).unwrap(); // ReadResult 持 unique_ptr,只能移出 - EXPECT_EQ(parsed.file->methods.size(), 1u); -} - -// ── -c 过滤:只处理匹配 class,导出 jar 保留未匹配 class ── -TEST(MjCliRunnerTest, classFilterPreservesUnmatchedJarResources) -{ - TempFile in(".jar"); - TempFile out(".jar"); - - model::ResourceContainer input; - input.addResource( - "Target.class", - std::make_unique("Target.class", buildMinimalClass("Target"))); - input.addResource( - "Other.class", - std::make_unique("Other.class", buildMinimalClass("Other"))); - ASSERT_TRUE(model::ResourceHelper::exportToJar(input, in.path()).is_ok()); - - CliOptions opts; - opts.input = in.path(); - opts.output = out.path(); - opts.targetClass = "Target"; - CliRunReport report = - runCli(opts, [](std::string_view, std::vector&) { return ClassProcessResult{}; }); - - EXPECT_EQ(report.processed, 1); - EXPECT_EQ(report.failed, 0); - EXPECT_EQ(report.failure, CliFailureKind::None); - EXPECT_TRUE(report.ok()); - - model::ResourceContainer reloaded; - ASSERT_TRUE(model::ResourceHelper::importFromJar(reloaded, out.path()).is_ok()); - EXPECT_TRUE(reloaded.hasResource("Target.class")); - EXPECT_TRUE(reloaded.hasResource("Other.class")); -} - -// ── processor 失败时计入 failed ── -TEST(MjCliRunnerTest, processorFailureCounted) -{ - TempFile in(".class"); - TempFile out(".class"); - - auto bytes = buildMinimalClass(); - ASSERT_TRUE( - FileUtil::write_bytes(in.path(), ca::core::ByteSlice(bytes.data(), bytes.size())).is_ok()); - - ClassProcessor proc = [](std::string_view, std::vector&) { - ClassProcessResult r; - r.ok = false; - r.message = "boom"; - return r; - }; - - CliOptions opts; - opts.input = in.path(); - opts.output = out.path(); - testing::internal::CaptureStderr(); - CliRunReport report = runCli(opts, proc); - std::string err = testing::internal::GetCapturedStderr(); - - EXPECT_EQ(report.processed, 0); - EXPECT_EQ(report.failed, 1); - EXPECT_EQ(report.failure, CliFailureKind::Processing); - EXPECT_EQ(report.failureMessage, "boom"); - EXPECT_FALSE(report.ok()); - EXPECT_NE(err.find("FAIL:"), std::string::npos); - EXPECT_EQ(err.find("no class files found"), std::string::npos); - EXPECT_FALSE(out.exists()); -} - -TEST(MjCliRunnerTest, verificationRejectsMalformedProcessorOutput) -{ - TempFile in(".class"); - TempFile out(".class"); - const auto input = buildMinimalClass(); - ASSERT_TRUE(FileUtil::write_bytes( - in.path(), ca::core::ByteSlice(input.data(), input.size())) - .is_ok()); - - ClassProcessor proc = [](std::string_view, std::vector& bytes) { - bytes = {0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x00}; - return ClassProcessResult{}; - }; - - CliOptions opts; - opts.input = in.path(); - opts.output = out.path(); - const auto report = runCli(opts, proc); - - EXPECT_FALSE(report.ok()); - EXPECT_EQ(report.failure, CliFailureKind::Verification); - EXPECT_EQ(report.processed, 0); - EXPECT_EQ(report.failed, 1); - EXPECT_FALSE(out.exists()); -} - -TEST(MjCliRunnerTest, toleratesJarClassEntryWithoutClassMagic) -{ - TempFile in(".jar"); - TempFile out(".jar"); - model::ResourceContainer input; - const auto goodClass = buildMinimalClass(); - const std::vector invalidClass = {0x00, 0x01, 0x02, 0x03}; - input.addResource("Good.class", std::make_unique("Good.class", goodClass)); - input.addResource( - "Invalid.class", std::make_unique("Invalid.class", invalidClass)); - ASSERT_TRUE(model::ResourceHelper::exportToJar(input, in.path()).is_ok()); - - CliOptions opts; - opts.input = in.path(); - opts.output = out.path(); - const auto report = - runCli(opts, [](std::string_view, std::vector&) { return ClassProcessResult{}; }); - - // 病态 jar 中“以 .class 命名但内容无 class magic”的 entry 按内容分拣为 - // UnknownResource(如被混淆器改坏的 module-info.class),容错跳过; - // 有效 class 存在时输出仍视为可验证通过,且输出文件产生。 - EXPECT_TRUE(report.ok()) << report.failureMessage; - EXPECT_EQ(report.processed, 1); - EXPECT_EQ(report.failed, 0); - EXPECT_TRUE(out.exists()); -} - -TEST(MjCliRunnerTest, refusesToOverwriteExistingOutput) -{ - TempFile in(".class"); - TempFile out(".class"); - const std::vector existingOutput = {0x01, 0x02, 0x03}; - const auto input = buildMinimalClass(); - ASSERT_TRUE(FileUtil::write_bytes( - in.path(), ca::core::ByteSlice(input.data(), input.size())) - .is_ok()); - ASSERT_TRUE(FileUtil::write_bytes( - out.path(), - ca::core::ByteSlice(existingOutput.data(), existingOutput.size())) - .is_ok()); - - CliOptions opts; - opts.input = in.path(); - opts.output = out.path(); - testing::internal::CaptureStderr(); - const auto report = - runCli(opts, [](std::string_view, std::vector&) { return ClassProcessResult{}; }); - const auto error = testing::internal::GetCapturedStderr(); - - EXPECT_FALSE(report.ok()); - EXPECT_EQ(report.failure, CliFailureKind::InvalidArguments); - EXPECT_NE(error.find("refusing to overwrite"), std::string::npos); - const auto actual = FileUtil::read_all_bytes(out.path()); - ASSERT_TRUE(actual.is_ok()); - const auto bytes = actual.unwrap(); - ASSERT_EQ(bytes.len(), existingOutput.size()); - EXPECT_TRUE(std::equal(bytes.as_ptr(), bytes.as_ptr() + bytes.len(), existingOutput.begin())); -} - -// ── u8path 清扫:UTF-8 输出路径 staged 发布钉 ───────────────── -// createStagedOutput 曾按 ACP 隐式窄构造 destination 且 generic_string 回写 -// 窄串(FileUtil 以 UTF-8 契约再解码 = 二次转码);非 UTF-8 ACP 进程里 UTF-8 -// 输出路径的 staged 文件落 mojibake 位、发布失败或错位。修复后 u8path 显式 -// 构造 + generic_u8string 全程 UTF-8 字节域——发布必须精确落在真实 UTF-8 路径。 -TEST(MjCliRunnerTest, stagedPublishesToExactUtf8OutputPath) -{ - namespace fs = std::filesystem; - - const auto outDir = - fs::temp_directory_path() / fs::u8path("mjbase_cli_u8_\xE7\xBC\x96\xE7\xA0\x81"); - fs::remove_all(outDir); - fs::create_directories(outDir); - // "输出.class"(UTF-8 手工字节转义)。 - const auto outPath = outDir / fs::u8path("\xE8\xBE\x93\xE5\x87\xBA.class"); - - TempFile in(".class"); - auto bytes = buildMinimalClass(); - ASSERT_TRUE( - FileUtil::write_bytes(in.path(), ca::core::ByteSlice(bytes.data(), bytes.size())) - .is_ok()); - - CliOptions opts; - opts.input = in.path(); - opts.output = outPath.u8string(); - const auto report = runCli(opts, [](std::string_view, std::vector&) { - return ClassProcessResult{}; - }); - - EXPECT_TRUE(report.ok()) << static_cast(report.failure); - EXPECT_EQ(report.processed, 1); - ASSERT_TRUE(fs::exists(outPath)) << "staged 发布必须精确落在 UTF-8 输出路径"; - const auto reparsed = FileUtil::read_all_bytes(outPath.u8string()); - ASSERT_TRUE(reparsed.is_ok()); - EXPECT_GT(reparsed.unwrap().len(), 0U); - - fs::remove_all(outDir); -} - -// ── targetMethod 解析:name / name()I 过滤语义(mbase -// buildMethodPredicate 的 (name, desc) 视图形态)── -TEST(MjCliRunnerTest, methodPredicateMatchesNameAndDescriptor) -{ - auto byName = MethodFilter::parse("keep"); - ASSERT_TRUE(byName.has_value()); - EXPECT_TRUE(byName->matches("keep", "()I")); - EXPECT_TRUE(byName->matches("keep", "()V")); - EXPECT_FALSE(byName->matches("other", "()I")); - - auto byDescriptor = MethodFilter::parse("keep()I"); - ASSERT_TRUE(byDescriptor.has_value()); - EXPECT_TRUE(byDescriptor->matches("keep", "()I")); - EXPECT_FALSE(byDescriptor->matches("keep", "()V")); - EXPECT_FALSE(byDescriptor->matches("other", "()I")); - - // 空串 → nullopt(不过滤),对应 mbase 的 nullptr 谓词。 - EXPECT_FALSE(MethodFilter::parse("").has_value()); -} diff --git a/mjbase/test/unittest/cli/keep_cli_test.cpp b/mjbase/test/unittest/cli/keep_cli_test.cpp deleted file mode 100644 index 8f9cf68..0000000 --- a/mjbase/test/unittest/cli/keep_cli_test.cpp +++ /dev/null @@ -1,123 +0,0 @@ -/// @file keep_cli_test.cpp -/// @brief keep 命令行暴露面用例(审计 P0#1)。 -/// -/// 覆盖:长名/短名/等号三种写法、缺失即留空、档位文本 → 枚举、未知档位 -/// fail-loud。本面只做搬运与档位解析,规则文件的读入在命令侧——故用例只 -/// 断言路径被搬进 CliOptions,不做任何文件系统访问。 -#include - -#include -#include -#include - -#include "mjbase/cli/arg_parser.hpp" -#include "mjbase/cli/keep_cli.hpp" - -using mjbase::cli::ArgParser; -using mjbase::cli::ArgResult; -using mjbase::cli::CliOptions; -using mjbase::cli::OptKind; -using mjbase::reference::KeepRuleErrorCode; -using mjbase::reference::KeepTier; - -namespace { - -/// 按 ArgParser 的 argv 契约装配参数串(首参为命令名)。 -std::optional parseArgs(const ArgParser& parser, - const std::vector& args) -{ - std::vector storage(args); - std::vector argv; - argv.push_back(const_cast("cmd")); - for (std::string& s : storage) argv.push_back(const_cast(s.c_str())); - return parser.parse(static_cast(argv.size()), argv.data()); -} - -ArgParser makeParser() -{ - ArgParser parser("cmd -i -o ", "keep 暴露面测试"); - mjbase::cli::addKeepOptions(parser); - return parser; -} - -// ── 解析 → CliOptions ────────────────────────────────────────── - -TEST(MjKeepCliTest, parsesKeepFileAndTierByLongName) -{ - const ArgParser parser = makeParser(); - const auto result = parseArgs(parser, {"--keep", "rules.txt", - "--keep-tier", "public"}); - ASSERT_TRUE(result.has_value()); - CliOptions options; - mjbase::cli::applyKeepOptions(*result, options); - EXPECT_EQ(options.keepFile, "rules.txt"); - EXPECT_EQ(options.keepTier, "public"); -} - -TEST(MjKeepCliTest, acceptsShortAliasAndEqualsForm) -{ - { - const ArgParser parser = makeParser(); - const auto result = parseArgs(parser, {"-k", "rules.txt"}); - ASSERT_TRUE(result.has_value()); - CliOptions options; - mjbase::cli::applyKeepOptions(*result, options); - EXPECT_EQ(options.keepFile, "rules.txt"); - EXPECT_TRUE(options.keepTier.empty()); - } - { - const ArgParser parser = makeParser(); - const auto result = parseArgs(parser, {"--keep=rules.txt", "--keep-tier=public"}); - ASSERT_TRUE(result.has_value()); - CliOptions options; - mjbase::cli::applyKeepOptions(*result, options); - EXPECT_EQ(options.keepFile, "rules.txt"); - EXPECT_EQ(options.keepTier, "public"); - } -} - -TEST(MjKeepCliTest, absentOptionsLeaveFieldsEmpty) -{ - const ArgParser parser = makeParser(); - const auto result = parseArgs(parser, {}); - ASSERT_TRUE(result.has_value()); - CliOptions options; - mjbase::cli::applyKeepOptions(*result, options); - EXPECT_TRUE(options.keepFile.empty()); - EXPECT_TRUE(options.keepTier.empty()); -} - -// 未注册 keep option 的解析器:未知 option 解析失败(配置错误稳定失败)。 -TEST(MjKeepCliTest, unknownKeepOptionFailsParse) -{ - ArgParser parser("cmd", "无 keep 注册"); - const auto result = parseArgs(parser, {"--keep", "rules.txt"}); - EXPECT_FALSE(result.has_value()); -} - -// ── 档位文本 → 枚举 ──────────────────────────────────────────── - -TEST(MjKeepCliTest, tierTextResolvesToEnum) -{ - EXPECT_EQ(mjbase::cli::keepTierOf("").unwrap(), KeepTier::None); - EXPECT_EQ(mjbase::cli::keepTierOf("none").unwrap(), KeepTier::None); - EXPECT_EQ(mjbase::cli::keepTierOf("public").unwrap(), KeepTier::Public); - EXPECT_EQ(mjbase::cli::keepTierOf("public-protected").unwrap(), KeepTier::PublicProtected); - EXPECT_EQ(mjbase::cli::keepTierOf("public_protected").unwrap(), KeepTier::PublicProtected); -} - -// 未知档位 fail-loud:不静默降级为 none(否则库型 JAR 的 protected 面会被删)。 -TEST(MjKeepCliTest, unknownTierFailsLoud) -{ - const auto r = mjbase::cli::keepTierOf("public-er"); - ASSERT_TRUE(r.is_err()); - EXPECT_EQ(r.unwrap_err(), KeepRuleErrorCode::UnknownTier); - - CliOptions options; - options.keepTier = "PUBLIC"; // 大小写敏感:拼错即失败,不猜测 - const auto fromOptions = mjbase::cli::keepTierOf(options); - ASSERT_TRUE(fromOptions.is_err()); - EXPECT_EQ(fromOptions.unwrap_err(), KeepRuleErrorCode::UnknownTier); -} - -} // namespace diff --git a/mjbase/test/unittest/cli/output_sink_test.cpp b/mjbase/test/unittest/cli/output_sink_test.cpp deleted file mode 100644 index c9fc6ff..0000000 --- a/mjbase/test/unittest/cli/output_sink_test.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include - -#include -#include - -#include "mjbase/cli/output_sink.hpp" - -using namespace mjbase::cli; - -namespace { - -// 用 tmpfile 捕获输出,读回字符串断言。tmpfile 在关闭时自动删除。 -std::string captureOutput(void (*fn)(OutputSink&)) -{ - FILE* f = std::tmpfile(); - if (!f) return ""; - OutputSink sink(f); - fn(sink); - std::fflush(f); - std::rewind(f); - std::string out; - char buf[256]; - while (size_t n = std::fread(buf, 1, sizeof(buf), f)) { - out.append(buf, n); - } - std::fclose(f); - return out; -} - -} // namespace - -TEST(MjOutputSinkTest, PrintfBasic) -{ - auto result = captureOutput([](OutputSink& s) { s.printf("Version: %d.%d\n", 52, 0); }); - EXPECT_EQ(result, "Version: 52.0\n"); -} - -TEST(MjOutputSinkTest, PrintfStringArg) -{ - auto result = - captureOutput([](OutputSink& s) { s.printf("Error: %s\n", std::string("boom").c_str()); }); - EXPECT_EQ(result, "Error: boom\n"); -} - -TEST(MjOutputSinkTest, PutsAddsNewline) -{ - auto result = captureOutput([](OutputSink& s) { s.puts("hello"); }); - EXPECT_EQ(result, "hello\n"); -} - -TEST(MjOutputSinkTest, WriteNoNewline) -{ - auto result = captureOutput([](OutputSink& s) { - s.write("a"); - s.write("b"); - }); - EXPECT_EQ(result, "ab"); -} - -TEST(MjOutputSinkTest, FileAccessor) -{ - FILE* f = std::tmpfile(); - OutputSink sink(f); - EXPECT_EQ(sink.file(), f); - std::fclose(f); -} - -// ── 全局 out()/err() 默认指向 stdout/stderr ──────────────────── -TEST(MjOutputSinkTest, GlobalOutPointsToStdout) -{ - EXPECT_EQ(out().file(), stdout); - EXPECT_EQ(err().file(), stderr); -} - -// ── parseFormat ──────────────────────────────────────────────── -TEST(MjParseFormatTest, KnownFormats) -{ - EXPECT_EQ(parseFormat("json"), Format::Json); - EXPECT_EQ(parseFormat("csv"), Format::Csv); - EXPECT_EQ(parseFormat("plain"), Format::Plain); -} - -TEST(MjParseFormatTest, UnknownDefaultsToPlain) -{ - EXPECT_EQ(parseFormat("xml"), Format::Plain); - EXPECT_EQ(parseFormat(""), Format::Plain); -} diff --git a/mjbase/test/unittest/i18n/i18n_test.cpp b/mjbase/test/unittest/i18n/i18n_test.cpp deleted file mode 100644 index dd088c3..0000000 --- a/mjbase/test/unittest/i18n/i18n_test.cpp +++ /dev/null @@ -1,23 +0,0 @@ -/// @file i18n_test.cpp -/// @brief i18n 兜底门面编译冒烟 + 兜底注册行为抽检。 -/// -/// 对 i18n.hpp 的单测以编译通过为主, -/// 抽检验证兜底注册后 key 可解析。 -#include - -#include - -#include "libca/i18n/i18n.hpp" - -#include "mjbase/i18n.hpp" - -TEST(MjI18nTest, ensureI18nBootRegistersFallback) -{ - mjbase::ensureI18nBoot(); - EXPECT_TRUE(ca::i18n::initialized()); - - const char* text = mjbase::tr("mbase.cli.usage"); - EXPECT_NE(text, nullptr); - // 兜底表已注册:不再裸 key 泄漏(key 空间保持 mbase.*,文案 zh_CN)。 - EXPECT_NE(std::string(text), "mbase.cli.usage"); -} diff --git a/mjbase/xmake.lua b/mjbase/xmake.lua index 3a250cb..6142805 100644 --- a/mjbase/xmake.lua +++ b/mjbase/xmake.lua @@ -5,30 +5,8 @@ -- 迁移期约定:源文件与测试文件走 glob 收编(多 agent 并行落文件), -- 模块稳定后按仓惯例转显式清单。 --- i18n 生成头回源:libca.i18n.embed-lang 将 .lang 文案编译为 --- i18n_mjbase_translations.generated.hpp(落于构建树 .gens),公开头 --- i18n.hpp 同目录直引。包导出只捕源树(add_headerfiles("src/(mjbase/**)")), --- 故构建后把生成件按内容判等同步回 src/mjbase/,供安装面捕获;内容 --- 未变不落盘,避免 mtime 抖动触发消费方重编级联。 -rule("mjbase.i18n_src_sync") - after_build(function (target) - import("core.project.config") - local name = "i18n_mjbase_translations.generated.hpp" - local gens = os.files(path.join(config.builddir(), "**", name)) - if not gens or #gens == 0 then - raise("mjbase.i18n_src_sync: 构建树内未找到 " .. name) - end - local dst = path.join(os.scriptdir(), "src", "mjbase", name) - local data = io.readfile(gens[1]) - if data ~= (os.isfile(dst) and io.readfile(dst) or nil) then - io.writefile(dst, data) - print(" > mjbase.i18n_src_sync: 同步 %s 至 src/mjbase/", name) - end - end) - target("mjbase") set_kind("static") - add_rules("mjbase.i18n_src_sync") -- plugin 构建戳:git 提交哈希编译期注入 build_stamp.h(仅 -- src/mjbase/plugin/build_stamp.cpp 消费),plugin 同源门据此在装配期 @@ -112,15 +90,12 @@ target("mjbase") add_files("src/mjbase/pass/**.cpp") add_files("src/mjbase/plugin/**.cpp") add_files("src/mjbase/asm/remapper/**.cpp") - -- 批 4:mjt 与 mjt-py 的直接消费面——renamer、jasm、log、cli。 - -- (workspace 已整体退役移交下游,jasm 仍留本批供 cli/验证面消费; - -- cli 只收源码(文案表 .lang 的规则在上方始终保留)。) + -- 批 4:mjt 与 mjt-py 的直接消费面——renamer、jasm、log。 + -- (workspace/cli 均已退役:workspace 引擎移交下游 mjt,cli 解析轮由 + -- libca ca::opt 承担、OutputSink 移交 mjt。) add_files("src/mjbase/renamer/**.cpp") add_files("src/mjbase/jasm/**.cpp") - add_files("src/mjbase/cli/*.cpp") -- cli 的 ArgParser 文案单源表(i18n_setup 静态注册,public include 供下游覆盖)。 - -- 批 1 不含 cli 源码,但 i18n.hpp 依赖其生成头,故规则始终保留。 - add_files("src/mjbase/cli/translations/*.lang", {rules = "libca.i18n.embed-lang"}) -- 导出捕获含 mjbase/ 前缀(src/(mjbase/**)):安装根下保留 mjbase/ 子树, -- 消费方 #include "mjbase/keys.hpp" 等伞头/模块头统一解析(同 libjbytecode -- src/(jbytecode/*.hpp) 口径)。 @@ -178,15 +153,13 @@ target("mjbase_unittest") -- golden 对拍子目录(旧栈 baseline 字节/指纹对拍机器,pass-golden-pipeline.md)。 add_files("test/unittest/pass/golden/*_test.cpp") add_files("test/unittest/asm/remapper/*_test.cpp") - -- 批 4:与上面 src 的 renamer/jasm/log/cli 一一对应(workspace 测试随模块退役)。 + -- 批 4:与上面 src 的 renamer/jasm/log 一一对应(workspace/cli 测试随模块退役)。 add_files("test/unittest/renamer/*_test.cpp") add_files("test/unittest/jasm/*_test.cpp") add_files("test/unittest/log/*_test.cpp") - add_files("test/unittest/cli/*_test.cpp") -- 第 5 批(孤儿测试救回):keys/i18n/util 伞头与叶子件, -- 与 src 的 keys.hpp/i18n.hpp/util 一一对应(B3-A 波次测试面,副本删除前收容)。 add_files("test/unittest/keys/*_test.cpp") - add_files("test/unittest/i18n/*_test.cpp") add_files("test/unittest/util/*_test.cpp") add_includedirs("src") -- 共享测试夹具目录(test/unittest/shared/):跨模块单测共用的头文件面,