From 62e719a753ad865dfe3acdc6632110b02d940806 Mon Sep 17 00:00:00 2001 From: Akio Jinsenji Date: Tue, 12 May 2026 01:03:02 +0900 Subject: [PATCH] =?UTF-8?q?feat(reference):=20member-level=20=E3=82=B7?= =?UTF-8?q?=E3=83=B3=E3=83=9C=E3=83=AB=E6=8A=BD=E5=87=BA=20MVP=EF=BC=88Pha?= =?UTF-8?q?se=204-A=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SPEC #191 (Phase 4 umbrella) のサブタスク A を最小 MVP として land する。 `extract_symbols_from_text` を拡張し、`public` / `internal` / `protected` / `private` で始まる method と property を index に含め られるようにする。Phase 2 の `reference find-symbol --kind method` が「Animator.Play」のような典型シンボルを直接 lookup できる。 主な変更: - src/reference/index.rs::extract_symbols_from_text: METHOD_RE と PROPERTY_RE を追加し、既存の TYPE_RE と組み合わせて symbol を抽出。 既に capture 済みの span (type declaration) は overlaps_consumed で skip し、class 名が method として double-capture されないように する。 - 新規 RED テスト 3 件: - extract_symbols_from_text_finds_methods: void / static int の 両方のシグネチャを method として抽出 - extract_symbols_from_text_finds_property: { get; set; } と { get { ... } } の両方を property として抽出 - extract_symbols_from_text_does_not_double_capture_class_name: class 名が method として捕捉されない回帰防止 スコープ外(umbrella #191 で継続管理): - 多行シグネチャ(戻り値型と method 名が改行で分かれるケース) - constructor / destructor(戻り値型を持たないため現 regex は通らない) - field / event 抽出 - ジェネリック制約句 `where T : ...` の取り扱い - record / record struct のような新しい型構文 regex MVP は偽陽性を含む可能性があるが、`--kind method` / `--kind property` のフィルタで noise を抑えやすい。本格的な精度は Phase 4 の 後追い改善か、別 SPEC として Roslyn 経由の解析を立てる余地がある。 ローカル検証: - cargo fmt -- --check: clean - cargo clippy --all-targets -- -D warnings: clean - cargo test --bin unity-cli: 392 passed / 0 failed - cargo llvm-cov --all-targets: TOTAL line coverage 90.89% - unity-cli skills lint --severity error: 15 skills / 0 violations Refs #191 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/reference/index.rs | 112 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/src/reference/index.rs b/src/reference/index.rs index 56494e4..aa8ebe0 100644 --- a/src/reference/index.rs +++ b/src/reference/index.rs @@ -58,6 +58,8 @@ impl Default for ReferenceSymbolIndex { pub fn extract_symbols_from_text(content: &str, rel_path: &str) -> Vec { static NAMESPACE_RE: OnceLock = OnceLock::new(); static TYPE_RE: OnceLock = OnceLock::new(); + static METHOD_RE: OnceLock = OnceLock::new(); + static PROPERTY_RE: OnceLock = OnceLock::new(); let ns_re = NAMESPACE_RE.get_or_init(|| { Regex::new(r"(?m)^\s*namespace\s+([A-Za-z_][A-Za-z0-9_.]*)") .expect("namespace regex compiles") @@ -68,12 +70,25 @@ pub fn extract_symbols_from_text(content: &str, rel_path: &str) -> Vec,\?\[\]\. \t]*?)[ \t]+([A-Za-z_][A-Za-z0-9_]*)[ \t]*\(", + ) + .expect("method regex compiles") + }); + let property_re = PROPERTY_RE.get_or_init(|| { + Regex::new( + r"(?m)^[ \t]*(?:\[[^\]]*\][ \t]*)*(?:public|internal|protected|private)[ \t]+(?:(?:static|virtual|override|abstract|sealed|partial|readonly)[ \t]+)*(?:[A-Za-z_][\w<>,\?\[\]\. \t]*?)[ \t]+([A-Za-z_][A-Za-z0-9_]*)[ \t]*\{[ \t]*(?:get|set)", + ) + .expect("property regex compiles") + }); let namespace = ns_re .captures(content) .and_then(|c| c.get(1).map(|m| m.as_str().to_string())); let mut symbols = Vec::new(); + let mut consumed_spans: Vec<(usize, usize)> = Vec::new(); for cap in type_re.captures_iter(content) { let kind = cap .get(1) @@ -87,6 +102,7 @@ pub fn extract_symbols_from_text(content: &str, rel_path: &str) -> Vec Vec bool { + spans.iter().any(|(start, end)| pos >= *start && pos < *end) +} + pub fn file_signature(metadata: &fs::Metadata) -> String { let mtime = metadata .modified() @@ -285,6 +354,49 @@ mod tests { .any(|s| s.name == "Vec3" && s.kind == "struct")); } + #[test] + fn extract_symbols_from_text_finds_methods() { + let text = "namespace UnityEngine {\n public class Animator {\n public void Play(string stateName) {}\n public static int GetLayerCount() { return 0; }\n }\n}\n"; + let symbols = extract_symbols_from_text(text, "Runtime/Animator.cs"); + let play = symbols + .iter() + .find(|s| s.name == "Play") + .expect("Play should be extracted"); + assert_eq!(play.kind, "method"); + assert_eq!(play.namespace.as_deref(), Some("UnityEngine")); + assert!(symbols + .iter() + .any(|s| s.name == "GetLayerCount" && s.kind == "method")); + assert!(symbols + .iter() + .any(|s| s.name == "Animator" && s.kind == "class")); + } + + #[test] + fn extract_symbols_from_text_finds_property() { + let text = + "public class Foo {\n public int Count { get; set; }\n public string Name { get { return _name; } }\n}\n"; + let symbols = extract_symbols_from_text(text, "Runtime/Foo.cs"); + assert!(symbols + .iter() + .any(|s| s.name == "Count" && s.kind == "property")); + assert!(symbols + .iter() + .any(|s| s.name == "Name" && s.kind == "property")); + assert!(!symbols + .iter() + .any(|s| s.name == "Count" && s.kind == "method")); + } + + #[test] + fn extract_symbols_from_text_does_not_double_capture_class_name() { + let text = "public class Bar {}\n"; + let symbols = extract_symbols_from_text(text, "Runtime/Bar.cs"); + let bar_entries: Vec<_> = symbols.iter().filter(|s| s.name == "Bar").collect(); + assert_eq!(bar_entries.len(), 1); + assert_eq!(bar_entries[0].kind, "class"); + } + #[test] fn build_or_update_index_persists_signature() { let tmp = TempDir::new().unwrap();