Skip to main content

libperl_macrogen/
apidoc_patches.rs

1//! Apidoc patches: data-driven corrections for known perl source bugs
2//!
3//! perl の C ヘッダや apidoc コメントには稀に誤りがある。例えば:
4//!
5//! - `cop.h` の `RCPV_LEN` `=for apidoc Am|RCPV *|RCPV_LEN|char *pv` は
6//!   戻り値型を `RCPV *` と謳っているが、実際の本体 `(RCPVx(pv)->len-1)` は
7//!   `STRLEN` を返す
8//! - `op.h` の `Perl_custom_op_xop(x)` マクロは `Perl_custom_op_get_field(x, ...)`
9//!   と展開されるが、`aTHX_` を渡し忘れているため Rust では引数数不一致 (E0061)
10//!
11//! 上流が修正されるまで、本モジュールは外部 JSON ファイル
12//! (`apidoc/v$ver.patches.json`) から訂正情報を読み込み、apidoc データの
13//! `return_type` 上書きや codegen 抑制を行う。
14//!
15//! ## ファイル形式
16//!
17//! ```json
18//! {
19//!     "schema_version": 1,
20//!     "comment": "free-form comment",
21//!     "patches": [
22//!         {
23//!             "name": "RCPV_LEN",
24//!             "kind": "return_type_override",
25//!             "value": "STRLEN",
26//!             "source_loc": "/usr/lib64/perl5/CORE/cop.h:560",
27//!             "reason": "apidoc claims `RCPV *` but body returns len-1 (STRLEN)",
28//!             "upstream_status": "to-report"
29//!         },
30//!         {
31//!             "name": "Perl_custom_op_xop",
32//!             "kind": "skip_codegen",
33//!             "source_loc": "/usr/lib64/perl5/CORE/op.h:977",
34//!             "reason": "macro lacks aTHX_; would generate 2-arg call to 3-arg fn",
35//!             "upstream_status": "to-report"
36//!         }
37//!     ]
38//! }
39//! ```
40//!
41//! ## 設計上の選択
42//!
43//! - **バージョン別**: `vX.Y.patches.json` で perl バージョンに紐付ける
44//!   (上流で修正されたら該当バージョンのファイルから消すだけで撤去可能)
45//! - **メイン apidoc JSON とは分離**: pre-generated `vX.Y.json` は perl-extract
46//!   等で再生成される可能性があり、手動編集は失われる。patches は手動メンテ用
47//! - **適用タイミング**:
48//!   - `return_type_override` / `arg_type_override`: apidoc load + inline merge 後
49//!   - `skip_codegen`: マクロ codegen 入口で early-return
50//!
51//! ## 既知の限界
52//!
53//! `kind` は初版で `return_type_override` と `skip_codegen` の 2 種のみ対応。
54//! 必要に応じて `arg_type_override`、`param_type_override`、`inject_thx_to_call`
55//! 等を追加する。
56//!
57//! ## add_decl (宣言の追加)
58//!
59//! 旧 perl のヘッダに `=for apidoc` コメントが無いマクロ (例: `AvARRAY`、
60//! `MUTABLE_PTR` 一族 — 5.38 でヘッダに追記された) は apidoc 辞書に載らず、
61//! 依存する Cv 一族などが cascade で消える (todo-2026-08-19.md)。
62//! `kind: "add_decl"` は「辞書にその名前が **無ければ** 宣言を追加、有れば
63//! no-op」を行う。宣言の契約が全バージョン同一なら `common.patches.json` に
64//! 1 セット置くだけでよく、新しい perl では自動的に no-op になる。
65//!
66//! ```json
67//! {
68//!     "name": "AvARRAY",
69//!     "kind": "add_decl",
70//!     "value": "Am|SV**|AvARRAY|AV* av",
71//!     "source_loc": "perl-5.42 av.h:80 (=for apidoc)",
72//!     "reason": "declaration added to headers in 5.38; contract identical in older perls",
73//!     "upstream_status": "fixed-in-5.38"
74//! }
75//! ```
76//!
77//! `value` は embed.fnc 形式の apidoc 行 (`flags|return_type|name|args...`。
78//! `=for apidoc ` / `=for apidoc_item ` プレフィックス付きでも可 — ヘッダから
79//! コピペできる)。ロード時に即パースし、パース不能・`name` 不一致は
80//! fail-fast。`skip_codegen` との同名共存は許容される (宣言で caller の
81//! 型推論を助けつつ、当該マクロ自身の wrapper 生成は抑制する組合せは正当)。
82
83use std::collections::{HashMap, HashSet};
84use std::io;
85use std::path::{Path, PathBuf};
86
87use serde::{Deserialize, Serialize};
88
89use crate::apidoc::{ApidocDict, ApidocEntry};
90
91/// `LIBPERL_MACROGEN_DEBUG_APIDOC=1` でデバッグ出力を有効化。
92pub(crate) fn is_apidoc_debug_enabled() -> bool {
93    std::env::var("LIBPERL_MACROGEN_DEBUG_APIDOC")
94        .map(|v| !v.is_empty() && v != "0")
95        .unwrap_or(false)
96}
97
98/// build script 経由で呼ばれた場合 `cargo:warning=` で CI ログに可視化する。
99/// CLI 直接実行(cargo run など)の場合は stderr にも複製し、両方の経路で
100/// 確認できるようにする。
101pub(crate) fn cargo_warning(msg: &str) {
102    println!("cargo:warning={}", msg);
103    eprintln!("{}", msg);
104}
105
106/// Patch ファイル全体
107#[derive(Debug, Clone, Default, Serialize, Deserialize)]
108pub struct ApidocPatchFile {
109    /// スキーマバージョン(互換性管理)
110    #[serde(default = "default_schema_version")]
111    pub schema_version: u32,
112    /// 自由記述コメント
113    #[serde(default)]
114    pub comment: Option<String>,
115    /// パッチ列
116    #[serde(default)]
117    pub patches: Vec<ApidocPatch>,
118}
119
120fn default_schema_version() -> u32 { 1 }
121
122/// 1 つのパッチエントリ
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ApidocPatch {
125    /// 対象 macro/function 名
126    pub name: String,
127    /// パッチ種別
128    pub kind: PatchKind,
129    /// `*_override` 系で必須の値
130    #[serde(default)]
131    pub value: Option<String>,
132    /// `arg_type_override` 用の引数 index
133    #[serde(default)]
134    pub arg_index: Option<usize>,
135    /// バグ箇所(デバッグ・上流報告用、`/path/to/file.h:line`)
136    #[serde(default)]
137    pub source_loc: Option<String>,
138    /// 何が間違っているか・なぜパッチが必要かの説明(必須)
139    pub reason: String,
140    /// 上流ステータス: "to-report" / "reported:URL" / "merged" / "fixed-in-5.42" 等
141    #[serde(default)]
142    pub upstream_status: Option<String>,
143}
144
145/// パッチ種別
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
147pub enum PatchKind {
148    /// apidoc entry の `return_type` を上書き
149    #[serde(rename = "return_type_override")]
150    ReturnTypeOverride,
151    /// apidoc entry の `args[arg_index].ty` を上書き(将来用)
152    #[serde(rename = "arg_type_override")]
153    ArgTypeOverride,
154    /// codegen 段階でこのマクロ/inline fn の生成を抑制し、
155    /// `[CODEGEN_SUPPRESSED]` コメントに置換
156    #[serde(rename = "skip_codegen")]
157    SkipCodegen,
158    /// 上位レイヤ(典型的には `common.patches.json`)で登録されているパッチを
159    /// 当該バージョンでだけ無効化する。`v$X.$Y.patches.json` で「上流が修正
160    /// された」ケースに使う。`value` などのフィールドは無視される。
161    #[serde(rename = "remove")]
162    Remove,
163    /// apidoc 辞書に宣言を追加する。**辞書に同名が既に有れば no-op**。
164    /// `value` に embed.fnc 形式の apidoc 行 (`flags|return_type|name|args...`、
165    /// `=for apidoc` プレフィックス付きも可) を書く。
166    /// 旧 perl のヘッダに `=for apidoc` が無い版非依存マクロの宣言を
167    /// `common.patches.json` で補うために使う (todo-2026-08-19.md)。
168    #[serde(rename = "add_decl")]
169    AddDecl,
170}
171
172/// ロード時にパース済みの add_decl 1 件
173#[derive(Debug, Clone)]
174pub struct AddDeclPatch {
175    /// `value` をパースした宣言。`source_file` にはパッチファイル名を
176    /// 埋めて出所を追跡可能にする
177    pub entry: ApidocEntry,
178    /// 必要理由 (JSON の `reason`)
179    pub reason: String,
180}
181
182/// ロード後の正規化された patch 集合(高速ルックアップ用)
183#[derive(Debug, Default)]
184pub struct ApidocPatchSet {
185    /// macro/fn 名 → (新 return_type, reason)
186    pub return_overrides: HashMap<String, (String, String)>,
187    /// macro/fn 名 → (arg_index, 新 ty, reason)
188    pub arg_overrides: HashMap<String, Vec<(usize, String, String)>>,
189    /// macro/fn 名 → reason(codegen 抑制対象)
190    pub skip_codegen: HashMap<String, String>,
191    /// macro/fn 名 → 追加する宣言(辞書に無い場合のみ適用)
192    pub add_decls: HashMap<String, AddDeclPatch>,
193    /// `kind: "remove"` で名指しされた、上位レイヤから取り除くべき名前。
194    /// 単一ファイル `load_json` 単独では実体に影響しないが、
195    /// `load_for_apidoc_path` の 2 段マージで version-specific から common
196    /// レイヤを打ち消すために使う。
197    pub removals: HashSet<String>,
198    /// ロードしたパッチファイルのパス(デバッグ用、ロード順)。
199    /// 2 段マージのときは `[common.patches.json, v$X.$Y.patches.json]`。
200    pub source_paths: Vec<PathBuf>,
201}
202
203/// テキスト形式の関数名リストファイルを読み込む。
204/// `--skip-codegen-list` / `--require-codegen-list` 共通の書式:
205///
206/// - 1 行に 1 つの関数名(マクロまたは inline 関数)
207/// - `#` 以降は comment として無視
208/// - 前後の空白はトリム、空行は無視
209///
210/// 出現順を保持して返す(重複除去はしない)。
211pub fn load_name_list<P: AsRef<Path>>(path: P) -> io::Result<Vec<String>> {
212    let content = std::fs::read_to_string(path.as_ref())?;
213    Ok(content
214        .lines()
215        .filter_map(|raw_line| {
216            let line = raw_line.split('#').next().unwrap_or("").trim();
217            if line.is_empty() {
218                None
219            } else {
220                Some(line.to_string())
221            }
222        })
223        .collect())
224}
225
226/// `add_decl` の `value` (apidoc 宣言行) をパースする。
227/// `=for apidoc ` / `=for apidoc_item ` プレフィックスは剥がして受理する
228/// (perl ヘッダからのコピペを想定)。型情報を持たない名前だけの行は
229/// 宣言として無意味なので None (ロード側で InvalidData になる)。
230fn parse_add_decl_value(value: &str) -> Option<ApidocEntry> {
231    let trimmed = value.trim();
232    if !trimmed.contains('|') {
233        return None;
234    }
235    if trimmed.starts_with("=for apidoc") {
236        ApidocEntry::parse_apidoc_line(trimmed)
237    } else {
238        ApidocEntry::parse_line(trimmed)
239    }
240}
241
242impl ApidocPatchSet {
243    pub fn empty() -> Self { Self::default() }
244
245    /// JSON ファイルから読み込み
246    pub fn load_json<P: AsRef<Path>>(path: P) -> io::Result<Self> {
247        let path_ref = path.as_ref();
248        let content = std::fs::read_to_string(path_ref)?;
249        let file: ApidocPatchFile = serde_json::from_str(&content).map_err(|e| {
250            io::Error::new(io::ErrorKind::InvalidData,
251                format!("apidoc patches JSON parse error: {}", e))
252        })?;
253        if file.schema_version != 1 {
254            return Err(io::Error::new(io::ErrorKind::InvalidData,
255                format!("unsupported apidoc patches schema_version: {}", file.schema_version)));
256        }
257        let mut set = Self::default();
258        set.source_paths.push(path_ref.to_path_buf());
259        for p in file.patches {
260            match p.kind {
261                PatchKind::ReturnTypeOverride => {
262                    let v = p.value.clone().ok_or_else(|| io::Error::new(
263                        io::ErrorKind::InvalidData,
264                        format!("patch for {}: return_type_override requires `value`", p.name)))?;
265                    set.return_overrides.insert(p.name, (v, p.reason));
266                }
267                PatchKind::ArgTypeOverride => {
268                    let v = p.value.clone().ok_or_else(|| io::Error::new(
269                        io::ErrorKind::InvalidData,
270                        format!("patch for {}: arg_type_override requires `value`", p.name)))?;
271                    let idx = p.arg_index.ok_or_else(|| io::Error::new(
272                        io::ErrorKind::InvalidData,
273                        format!("patch for {}: arg_type_override requires `arg_index`", p.name)))?;
274                    set.arg_overrides.entry(p.name).or_default().push((idx, v, p.reason));
275                }
276                PatchKind::SkipCodegen => {
277                    set.skip_codegen.insert(p.name, p.reason);
278                }
279                PatchKind::Remove => {
280                    // 単独 load では実体には影響しない(removals に記録するだけ)。
281                    // 2 段マージ時に上位レイヤから打ち消すために使われる。
282                    set.removals.insert(p.name);
283                }
284                PatchKind::AddDecl => {
285                    let v = p.value.clone().ok_or_else(|| io::Error::new(
286                        io::ErrorKind::InvalidData,
287                        format!("patch for {}: add_decl requires `value` \
288                                 (apidoc line: flags|return_type|name|args...)", p.name)))?;
289                    let mut entry = parse_add_decl_value(&v).ok_or_else(|| io::Error::new(
290                        io::ErrorKind::InvalidData,
291                        format!("patch for {}: add_decl `value` is not a parsable \
292                                 apidoc line: {:?}", p.name, v)))?;
293                    if entry.name != p.name {
294                        return Err(io::Error::new(io::ErrorKind::InvalidData,
295                            format!("patch for {}: add_decl `value` declares a different \
296                                     name `{}`", p.name, entry.name)));
297                    }
298                    entry.source_file = Some(format!("{} (add_decl)", path_ref.display()));
299                    set.add_decls.insert(p.name, AddDeclPatch { entry, reason: p.reason });
300                }
301            }
302        }
303        Ok(set)
304    }
305
306    /// `apidoc/v$major.$minor.patches.json` を解決して読み込み
307    /// ファイルが存在しない場合は空の patch set を返す(エラーにしない)
308    pub fn load_for_perl_version<P: AsRef<Path>>(
309        apidoc_dir: P, major: u32, minor: u32,
310    ) -> io::Result<Self> {
311        let filename = format!("v{}.{}.patches.json", major, minor);
312        let path = apidoc_dir.as_ref().join(&filename);
313        if !path.exists() {
314            return Ok(Self::empty());
315        }
316        Self::load_json(&path)
317    }
318
319    /// **2 段マージ版ローダ**: `apidoc_path` (`<dir>/v$X.$Y.json`) と同じ
320    /// ディレクトリにある以下の 2 ファイルを優先順位付きで読み込む:
321    ///
322    /// 1. **`<dir>/common.patches.json`** — 全バージョン共通のパッチ
323    /// 2. **`<dir>/v$X.$Y.patches.json`** — 当該バージョン固有のパッチ
324    ///
325    /// マージ規則:
326    /// - 同一 `name` のエントリは **後者(version-specific)が前者(common)を上書き**
327    /// - version-specific 側の `kind: "remove"` は common 側の同名エントリを **削除**
328    ///   (上流で fix されたバージョンでパッチを撤去する用途)
329    ///
330    /// 両ファイルとも存在しない場合は空 set を返す(エラーにしない)。
331    pub fn load_for_apidoc_path<P: AsRef<Path>>(apidoc_path: P) -> io::Result<Self> {
332        let path_ref = apidoc_path.as_ref();
333        let dir = path_ref.parent().unwrap_or_else(|| Path::new("."));
334        let debug = is_apidoc_debug_enabled();
335
336        let mut set = Self::default();
337
338        if debug {
339            cargo_warning(&format!(
340                "[apidoc-patches] load_for_apidoc_path: apidoc_path={}, dir={}",
341                path_ref.display(), dir.display()
342            ));
343        }
344
345        // 1. common.patches.json(あれば)
346        let common_path = dir.join("common.patches.json");
347        if common_path.exists() {
348            let common = Self::load_json(&common_path)?;
349            if debug {
350                cargo_warning(&format!(
351                    "[apidoc-patches] loaded common.patches.json: \
352                     {} return_overrides, {} arg_overrides, {} skip_codegen, \
353                     {} add_decls, {} removals",
354                    common.return_overrides.len(),
355                    common.arg_overrides.len(),
356                    common.skip_codegen.len(),
357                    common.add_decls.len(),
358                    common.removals.len(),
359                ));
360            }
361            set.merge_overlay(common);
362        } else if debug {
363            cargo_warning(&format!(
364                "[apidoc-patches] common.patches.json NOT FOUND at {}",
365                common_path.display()
366            ));
367        }
368
369        // 2. v$X.$Y.patches.json(あれば)
370        let version_path = {
371            let stem = path_ref.file_stem()
372                .map(|s| s.to_string_lossy().into_owned())
373                .unwrap_or_else(String::new);
374            path_ref.with_file_name(format!("{}.patches.json", stem))
375        };
376        if version_path.exists() {
377            let version = Self::load_json(&version_path)?;
378            if debug {
379                cargo_warning(&format!(
380                    "[apidoc-patches] loaded {}: \
381                     {} return_overrides, {} skip_codegen, {} add_decls, {} removals",
382                    version_path.file_name().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default(),
383                    version.return_overrides.len(),
384                    version.skip_codegen.len(),
385                    version.add_decls.len(),
386                    version.removals.len(),
387                ));
388            }
389            // 先に version 側の removals で common を打ち消す
390            for name in &version.removals {
391                set.return_overrides.remove(name);
392                set.arg_overrides.remove(name);
393                set.skip_codegen.remove(name);
394                set.add_decls.remove(name);
395            }
396            // それから version 側のパッチを上書きマージ
397            set.merge_overlay(version);
398        } else if debug {
399            cargo_warning(&format!(
400                "[apidoc-patches] {} NOT FOUND",
401                version_path.file_name().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default()
402            ));
403        }
404
405        Ok(set)
406    }
407
408    /// 別の patch set を **後勝ち** で重ね合わせる。
409    /// 同名の override は上書き、`source_paths` は追記、`removals` は和集合。
410    fn merge_overlay(&mut self, other: ApidocPatchSet) {
411        for (k, v) in other.return_overrides {
412            self.return_overrides.insert(k, v);
413        }
414        for (k, v) in other.arg_overrides {
415            // arg_overrides は配列。同名で上書きするときは置換(追加ではない)。
416            self.arg_overrides.insert(k, v);
417        }
418        for (k, v) in other.skip_codegen {
419            self.skip_codegen.insert(k, v);
420        }
421        for (k, v) in other.add_decls {
422            self.add_decls.insert(k, v);
423        }
424        for name in other.removals {
425            self.removals.insert(name);
426        }
427        self.source_paths.extend(other.source_paths);
428    }
429
430    /// テキスト形式の skip-list ファイルを読み込んで
431    /// skip_codegen に名前を追加する。
432    ///
433    /// フォーマット:
434    /// - 1 行に 1 つの関数名(マクロまたは inline 関数)
435    /// - `#` で始まる行は comment として無視
436    /// - 前後の空白はトリム、空行は無視
437    ///
438    /// 同名が既に存在する場合は **既存を優先**(JSON patches で設定済みなど)。
439    /// reason は `"skip-list: <filename>"` を埋め込む。
440    pub fn merge_skip_list<P: AsRef<Path>>(&mut self, path: P) -> io::Result<usize> {
441        let path_ref = path.as_ref();
442        let display_name = path_ref.file_name()
443            .map(|s| s.to_string_lossy().to_string())
444            .unwrap_or_else(|| path_ref.display().to_string());
445        let reason = format!("skip-list: {}", display_name);
446        let mut added = 0usize;
447        for name in load_name_list(path_ref)? {
448            // 既存(JSON patches 等)を優先、同名は上書きしない
449            if !self.skip_codegen.contains_key(&name) {
450                self.skip_codegen.insert(name, reason.clone());
451                added += 1;
452            }
453        }
454        Ok(added)
455    }
456
457    /// パッチが空(適用するものが無い)か
458    pub fn is_empty(&self) -> bool {
459        self.return_overrides.is_empty()
460            && self.arg_overrides.is_empty()
461            && self.skip_codegen.is_empty()
462            && self.add_decls.is_empty()
463    }
464
465    /// パッチ件数
466    pub fn count(&self) -> usize {
467        self.return_overrides.len()
468            + self.arg_overrides.iter().map(|(_, v)| v.len()).sum::<usize>()
469            + self.skip_codegen.len()
470            + self.add_decls.len()
471    }
472
473    /// `return_type_override` と `arg_type_override` を `ApidocDict` に適用
474    /// 適用された entry 名のリストを返す。対象が dict に存在しない場合は warning
475    /// として stderr に出力(perl 側で fix された等の状況検知用)。
476    ///
477    /// **デバッグ出力**: 環境変数 `LIBPERL_MACROGEN_DEBUG_APIDOC=1` を設定すると、
478    /// パッチ適用の hit/miss、適用前後の戻り値型、dict 全体の RCPV 関連エントリ等を
479    /// `cargo:warning=` 経由で出力する(build script 経由で呼ばれた場合は CI ログに
480    /// 可視化される)。CI で patch が一部バージョンで効かない問題の調査用。
481    /// **MISS は環境変数なしでも常に `cargo:warning=` として出力する**(黙って
482    /// 取りこぼされる事故を防ぐため)。
483    pub fn apply_to_apidoc(&self, dict: &mut ApidocDict) -> Vec<String> {
484        let debug = is_apidoc_debug_enabled();
485        let mut applied: Vec<String> = Vec::new();
486
487        if debug {
488            cargo_warning(&format!(
489                "[apidoc-patches] apply_to_apidoc: dict has {} entries; \
490                 patches: {} return_overrides, {} arg_overrides, {} skip_codegen, \
491                 {} add_decls",
492                dict.len(),
493                self.return_overrides.len(),
494                self.arg_overrides.len(),
495                self.skip_codegen.len(),
496                self.add_decls.len(),
497            ));
498        }
499
500        // add_decl は override 系より先に適用する
501        // (注入した宣言に return/arg override を重ねられる順序)
502        for (name, patch) in &self.add_decls {
503            if dict.get(name).is_none() {
504                dict.insert(name.clone(), patch.entry.clone());
505                applied.push(name.clone());
506                if debug {
507                    cargo_warning(&format!(
508                        "[apidoc-patches] add_decl ADDED `{}`: {} ({} args)",
509                        name,
510                        patch.entry.return_type.as_deref().unwrap_or("(none)"),
511                        patch.entry.args.len(),
512                    ));
513                }
514            } else if debug {
515                cargo_warning(&format!(
516                    "[apidoc-patches] add_decl `{}`: already present in dict, no-op",
517                    name
518                ));
519            }
520        }
521
522        for (name, (new_ty, _reason)) in &self.return_overrides {
523            if let Some(entry) = dict.get_mut(name) {
524                let old = entry.return_type.clone();
525                entry.return_type = Some(new_ty.clone());
526                applied.push(name.clone());
527                if debug {
528                    cargo_warning(&format!(
529                        "[apidoc-patches] return_type_override APPLIED `{}`: {} -> {}",
530                        name,
531                        old.as_deref().unwrap_or("(none)"),
532                        new_ty,
533                    ));
534                }
535            } else {
536                // MISS は env var 不要で常に可視化(黙って取りこぼされるのを防ぐ)
537                cargo_warning(&format!(
538                    "[apidoc-patches] return_type_override MISS `{}`: \
539                     target not found in apidoc dict (dict has {} entries) — \
540                     codegen falls back to whatever else is inferred",
541                    name, dict.len()
542                ));
543            }
544        }
545        for (name, list) in &self.arg_overrides {
546            if let Some(entry) = dict.get_mut(name) {
547                for (idx, new_ty, _reason) in list {
548                    if let Some(arg) = entry.args.get_mut(*idx) {
549                        arg.ty = new_ty.clone();
550                    } else {
551                        cargo_warning(&format!(
552                            "[apidoc-patches] arg_type_override `{}` arg_index {} \
553                             out of range (entry has {} args)",
554                            name, idx, entry.args.len()
555                        ));
556                    }
557                }
558                applied.push(name.clone());
559            } else {
560                cargo_warning(&format!(
561                    "[apidoc-patches] arg_type_override MISS `{}`: \
562                     target not found in apidoc dict",
563                    name
564                ));
565            }
566        }
567
568        // デバッグ時のみ、dict 内の patch 関連エントリ群を dump
569        // (inline merge が `=for apidoc` を拾えているかの判別用)
570        if debug {
571            let interest_prefixes: Vec<&str> = self.return_overrides.keys()
572                .chain(self.skip_codegen.keys())
573                .map(|s| s.as_str())
574                .collect();
575            let mut prefixes_set: std::collections::HashSet<&str> = std::collections::HashSet::new();
576            for n in &interest_prefixes {
577                // 共通プレフィックスを抽出(例: "RCPV_")。簡易的に "_" までの先頭部。
578                if let Some(idx) = n.find('_') {
579                    prefixes_set.insert(&n[..idx + 1]);
580                }
581            }
582            for prefix in prefixes_set {
583                let matches: Vec<String> = dict.iter()
584                    .filter(|(name, _)| name.starts_with(prefix))
585                    .map(|(name, e)| format!("{}->{}", name, e.return_type.as_deref().unwrap_or("?")))
586                    .collect();
587                cargo_warning(&format!(
588                    "[apidoc-patches] dict entries with prefix `{}` ({} entries): {:?}",
589                    prefix, matches.len(), matches
590                ));
591            }
592        }
593
594        applied
595    }
596
597    /// codegen 抑制対象なら reason を返す
598    pub fn skip_reason(&self, name: &str) -> Option<&str> {
599        self.skip_codegen.get(name).map(|s| s.as_str())
600    }
601}
602
603#[cfg(test)]
604mod tests {
605    use super::*;
606    use std::fs;
607    use std::io::Write;
608    use tempfile::TempDir;
609
610    fn write_json(dir: &Path, name: &str, content: &str) -> PathBuf {
611        let path = dir.join(name);
612        let mut f = fs::File::create(&path).unwrap();
613        f.write_all(content.as_bytes()).unwrap();
614        path
615    }
616
617    const COMMON_PATCH: &str = r#"{
618        "schema_version": 1,
619        "patches": [
620            { "name": "RCPV_LEN", "kind": "return_type_override",
621              "value": "STRLEN", "reason": "common: wrong apidoc" },
622            { "name": "Perl_custom_op_xop", "kind": "skip_codegen",
623              "reason": "common: macro lacks aTHX_" }
624        ]
625    }"#;
626
627    #[test]
628    fn test_load_name_list_parses_comments_and_blanks() {
629        let tmp = TempDir::new().unwrap();
630        let path = write_json(
631            tmp.path(),
632            "require.txt",
633            "# header comment\nCvFILE\n\n  CvROOT  # trailing comment\n#\nCvSTART\n",
634        );
635        let names = load_name_list(&path).unwrap();
636        assert_eq!(names, vec!["CvFILE", "CvROOT", "CvSTART"]);
637    }
638
639    #[test]
640    fn test_load_common_only() {
641        let tmp = TempDir::new().unwrap();
642        write_json(tmp.path(), "common.patches.json", COMMON_PATCH);
643        let apidoc_path = tmp.path().join("v5.40.json");
644        // v5.40.json 自体は存在しなくても OK(patches 解決はパスから派生するだけ)
645
646        let set = ApidocPatchSet::load_for_apidoc_path(&apidoc_path).unwrap();
647        assert_eq!(set.return_overrides.len(), 1);
648        assert_eq!(set.return_overrides["RCPV_LEN"].0, "STRLEN");
649        assert_eq!(set.skip_codegen.len(), 1);
650        assert!(set.skip_codegen.contains_key("Perl_custom_op_xop"));
651        assert_eq!(set.source_paths.len(), 1);
652    }
653
654    #[test]
655    fn test_version_overrides_common() {
656        let tmp = TempDir::new().unwrap();
657        write_json(tmp.path(), "common.patches.json", COMMON_PATCH);
658        // v5.42 で RCPV_LEN の戻り値型を別の値に上書き
659        let version_json = r#"{
660            "schema_version": 1,
661            "patches": [
662                { "name": "RCPV_LEN", "kind": "return_type_override",
663                  "value": "Size_t", "reason": "v5.42: tweaked" }
664            ]
665        }"#;
666        write_json(tmp.path(), "v5.42.patches.json", version_json);
667        let apidoc_path = tmp.path().join("v5.42.json");
668
669        let set = ApidocPatchSet::load_for_apidoc_path(&apidoc_path).unwrap();
670        // RCPV_LEN は version-specific が勝つ
671        assert_eq!(set.return_overrides["RCPV_LEN"].0, "Size_t");
672        // common 由来の Perl_custom_op_xop はそのまま残る
673        assert!(set.skip_codegen.contains_key("Perl_custom_op_xop"));
674        // ロードしたファイル数は 2
675        assert_eq!(set.source_paths.len(), 2);
676    }
677
678    #[test]
679    fn test_remove_kind_drops_common_entry() {
680        let tmp = TempDir::new().unwrap();
681        write_json(tmp.path(), "common.patches.json", COMMON_PATCH);
682        // v5.42 で Perl_custom_op_xop が修正されたとして打ち消す
683        let version_json = r#"{
684            "schema_version": 1,
685            "patches": [
686                { "name": "Perl_custom_op_xop", "kind": "remove",
687                  "reason": "fixed upstream in 5.42" }
688            ]
689        }"#;
690        write_json(tmp.path(), "v5.42.patches.json", version_json);
691        let apidoc_path = tmp.path().join("v5.42.json");
692
693        let set = ApidocPatchSet::load_for_apidoc_path(&apidoc_path).unwrap();
694        // Perl_custom_op_xop は removed
695        assert!(!set.skip_codegen.contains_key("Perl_custom_op_xop"));
696        // RCPV_LEN は common 由来でそのまま残る
697        assert!(set.return_overrides.contains_key("RCPV_LEN"));
698        // removals フィールドにも記録されている
699        assert!(set.removals.contains("Perl_custom_op_xop"));
700    }
701
702    #[test]
703    fn test_no_patches_files_returns_empty() {
704        let tmp = TempDir::new().unwrap();
705        let apidoc_path = tmp.path().join("v5.40.json");
706        let set = ApidocPatchSet::load_for_apidoc_path(&apidoc_path).unwrap();
707        assert!(set.is_empty());
708        assert_eq!(set.source_paths.len(), 0);
709    }
710
711    const ADD_DECL_PATCH: &str = r#"{
712        "schema_version": 1,
713        "patches": [
714            { "name": "AvARRAY", "kind": "add_decl",
715              "value": "Am|SV**|AvARRAY|AV* av",
716              "reason": "declaration added to headers in 5.38" },
717            { "name": "MUTABLE_PTR", "kind": "add_decl",
718              "value": "=for apidoc Am |void *|MUTABLE_PTR|void * p",
719              "reason": "prefix form should be accepted (copy-paste from header)" }
720        ]
721    }"#;
722
723    #[test]
724    fn test_add_decl_load() {
725        let tmp = TempDir::new().unwrap();
726        let path = write_json(tmp.path(), "common.patches.json", ADD_DECL_PATCH);
727        let set = ApidocPatchSet::load_json(&path).unwrap();
728        assert_eq!(set.add_decls.len(), 2);
729        assert_eq!(set.count(), 2);
730        assert!(!set.is_empty());
731
732        let av = &set.add_decls["AvARRAY"];
733        assert_eq!(av.entry.return_type.as_deref(), Some("SV**"));
734        assert_eq!(av.entry.args.len(), 1);
735        assert!(av.entry.flags.is_macro);
736        assert!(av.entry.source_file.as_deref().unwrap().contains("add_decl"));
737
738        // `=for apidoc ` プレフィックス付きの value も受理される
739        let mp = &set.add_decls["MUTABLE_PTR"];
740        assert_eq!(mp.entry.return_type.as_deref(), Some("void *"));
741    }
742
743    #[test]
744    fn test_add_decl_applies_when_absent() {
745        let tmp = TempDir::new().unwrap();
746        let path = write_json(tmp.path(), "common.patches.json", ADD_DECL_PATCH);
747        let set = ApidocPatchSet::load_json(&path).unwrap();
748
749        let mut dict = ApidocDict::new();
750        let applied = set.apply_to_apidoc(&mut dict);
751        assert!(applied.contains(&"AvARRAY".to_string()));
752        assert_eq!(dict.get("AvARRAY").unwrap().return_type.as_deref(), Some("SV**"));
753        assert!(dict.get("MUTABLE_PTR").is_some());
754    }
755
756    #[test]
757    fn test_add_decl_noop_when_present() {
758        let tmp = TempDir::new().unwrap();
759        let path = write_json(tmp.path(), "common.patches.json", ADD_DECL_PATCH);
760        let set = ApidocPatchSet::load_json(&path).unwrap();
761
762        // 辞書に既にヘッダ由来の (異なる) 宣言があるケース
763        let mut dict = ApidocDict::new();
764        let native = ApidocEntry::parse_line("Cm|SSize_t|AvARRAY|AV* av").unwrap();
765        dict.insert("AvARRAY".to_string(), native);
766
767        let applied = set.apply_to_apidoc(&mut dict);
768        // 既存エントリは上書きされない (no-op)
769        assert_eq!(dict.get("AvARRAY").unwrap().return_type.as_deref(), Some("SSize_t"));
770        assert!(!applied.contains(&"AvARRAY".to_string()));
771        // 不在だった MUTABLE_PTR は追加される
772        assert!(applied.contains(&"MUTABLE_PTR".to_string()));
773    }
774
775    #[test]
776    fn test_add_decl_value_errors() {
777        let tmp = TempDir::new().unwrap();
778        // value 欠落
779        let p1 = write_json(tmp.path(), "p1.patches.json", r#"{
780            "schema_version": 1,
781            "patches": [ { "name": "FOO", "kind": "add_decl", "reason": "r" } ]
782        }"#);
783        assert!(ApidocPatchSet::load_json(&p1).is_err());
784        // パース不能 (型情報の無い名前だけの行)
785        let p2 = write_json(tmp.path(), "p2.patches.json", r#"{
786            "schema_version": 1,
787            "patches": [ { "name": "FOO", "kind": "add_decl",
788                           "value": "FOO", "reason": "r" } ]
789        }"#);
790        assert!(ApidocPatchSet::load_json(&p2).is_err());
791        // name フィールドと value 内の名前の不一致
792        let p3 = write_json(tmp.path(), "p3.patches.json", r#"{
793            "schema_version": 1,
794            "patches": [ { "name": "FOO", "kind": "add_decl",
795                           "value": "Am|int|BAR|int x", "reason": "r" } ]
796        }"#);
797        assert!(ApidocPatchSet::load_json(&p3).is_err());
798    }
799
800    #[test]
801    fn test_add_decl_removed_by_version() {
802        let tmp = TempDir::new().unwrap();
803        write_json(tmp.path(), "common.patches.json", ADD_DECL_PATCH);
804        let version_json = r#"{
805            "schema_version": 1,
806            "patches": [
807                { "name": "AvARRAY", "kind": "remove",
808                  "reason": "this version must not inject AvARRAY" }
809            ]
810        }"#;
811        write_json(tmp.path(), "v5.42.patches.json", version_json);
812        let apidoc_path = tmp.path().join("v5.42.json");
813
814        let set = ApidocPatchSet::load_for_apidoc_path(&apidoc_path).unwrap();
815        assert!(!set.add_decls.contains_key("AvARRAY"));
816        assert!(set.add_decls.contains_key("MUTABLE_PTR"));
817    }
818
819    #[test]
820    fn test_add_decl_with_return_override() {
821        // add_decl で注入した宣言に return_type_override を重ねられる
822        // (add_decl が先に適用される順序の検証)
823        let tmp = TempDir::new().unwrap();
824        let json = r#"{
825            "schema_version": 1,
826            "patches": [
827                { "name": "AvARRAY", "kind": "add_decl",
828                  "value": "Am|SV**|AvARRAY|AV* av", "reason": "r" },
829                { "name": "AvARRAY", "kind": "return_type_override",
830                  "value": "SV *", "reason": "r" }
831            ]
832        }"#;
833        let path = write_json(tmp.path(), "common.patches.json", json);
834        let set = ApidocPatchSet::load_json(&path).unwrap();
835
836        let mut dict = ApidocDict::new();
837        set.apply_to_apidoc(&mut dict);
838        assert_eq!(dict.get("AvARRAY").unwrap().return_type.as_deref(), Some("SV *"));
839    }
840
841    #[test]
842    fn test_remove_only_in_singlefile_load_does_not_panic() {
843        // 単独 load_json で kind: "remove" を読んでも実体には影響しない
844        // (removals に記録されるだけ、override 系には触らない)
845        let tmp = TempDir::new().unwrap();
846        let json = r#"{
847            "schema_version": 1,
848            "patches": [
849                { "name": "FOO", "kind": "remove", "reason": "test" }
850            ]
851        }"#;
852        let path = write_json(tmp.path(), "v5.42.patches.json", json);
853        let set = ApidocPatchSet::load_json(&path).unwrap();
854        assert!(set.return_overrides.is_empty());
855        assert!(set.skip_codegen.is_empty());
856        assert!(set.removals.contains("FOO"));
857    }
858}