From 9daa28df3304777634e2dc30226b2c6a5f588932 Mon Sep 17 00:00:00 2001 From: Akio Jinsenji Date: Mon, 11 May 2026 23:49:40 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(reference):=20Phase=203=20-=20referenc?= =?UTF-8?q?e=20diff=20=E3=81=A8=20resolve-symbol-at=EF=BC=88SPEC=20#188?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gwt-discussion で確定した方針に従い、Phase 3 として次の 2 機能を land: 1. reference diff: 2 cached Unity バージョン間の symbol-only / path 範囲 差分。デフォルトは --symbol で symbol-only、--path で path 範囲 (added/removed/changed) を opt-in。 2. reference resolve-symbol-at: project ファイルの cursor 位置から reference cache の候補を返す独立 RPC。csharp-lsp には触らない。 設計判断(採用方針): - 差分粒度 C: symbol-only コア + --path 全体走査 opt-in。MVP の line diff は all-or-nothing 単一 hunk で簡素実装。 - LSP 連携 C2: unity-cli 単体の独立 RPC。SPEC #185 の workspaceFolders 不混入の原則を維持。 主な変更: - src/reference/diff.rs (新規): Hunk / SymbolDiff / PathDiff 型、 compute_line_diff / compute_symbol_diff / compute_path_diff / ensure_cache_dir / split_fqn / read_excerpt - src/reference/mod.rs: maybe_execute_reference_tool に reference_diff と reference_resolve_symbol_at 分岐、execute_diff / execute_resolve_symbol_at / extract_token_at_cursor / collect_resolve_candidates、dispatcher 経由 test 計 9 件 - src/cli.rs: ReferenceCommand::Diff / ResolveSymbolAt - src/app/runner.rs: build_reference_call に 2 分岐と 4 件のテスト - src/tooling/tool_catalog.rs: 2 tool を TOOL_NAMES / executor / read_only / params_schema / parity count (125→127) に登録 - skill SKILL.md: diff / resolve-symbol-at 例とフォールバック説明 - references/version-diff-playbook.md (新規): Phase 3 の活用ガイド - docs/tools.md: Reference Cache (7→9 tools) と新 tool 行 - tests/fixtures/reference-cache-v2/{v1,v2}/: 差分テスト用 fixture (Animator changed、LegacyAnimator removed、Awaitable added) ローカル検証: - cargo fmt -- --check: clean - cargo clippy --all-targets -- -D warnings: clean - cargo test --bin unity-cli: 379 passed / 0 failed - cargo llvm-cov --all-targets: TOTAL line coverage 90.97% - unity-cli skills lint --severity error: 15 skills / 0 violations Refs #188 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../skills/unity-csharp-reference/SKILL.md | 6 + .../references/version-diff-playbook.md | 96 +++++ docs/tools.md | 6 +- src/app/runner.rs | 100 +++++ src/cli.rs | 23 ++ src/reference/diff.rs | 385 ++++++++++++++++++ src/reference/mod.rs | 381 +++++++++++++++++ src/tooling/tool_catalog.rs | 32 +- .../v1/.unity-cli-index/symbols.json | 45 ++ .../v1/Editor/AnimatorInspector.cs | 9 + .../Export/Animation/Animator.bindings.cs | 9 + .../v1/Runtime/Export/Animation/Removed.cs | 6 + .../v2/.unity-cli-index/symbols.json | 45 ++ .../v2/Editor/AnimatorInspector.cs | 9 + .../Export/Animation/Animator.bindings.cs | 13 + .../v2/Runtime/Export/New/Awaitable.cs | 10 + 16 files changed, 1172 insertions(+), 3 deletions(-) create mode 100644 .claude-plugin/plugins/unity-cli/skills/unity-csharp-reference/references/version-diff-playbook.md create mode 100644 src/reference/diff.rs create mode 100644 tests/fixtures/reference-cache-v2/v1/.unity-cli-index/symbols.json create mode 100644 tests/fixtures/reference-cache-v2/v1/Editor/AnimatorInspector.cs create mode 100644 tests/fixtures/reference-cache-v2/v1/Runtime/Export/Animation/Animator.bindings.cs create mode 100644 tests/fixtures/reference-cache-v2/v1/Runtime/Export/Animation/Removed.cs create mode 100644 tests/fixtures/reference-cache-v2/v2/.unity-cli-index/symbols.json create mode 100644 tests/fixtures/reference-cache-v2/v2/Editor/AnimatorInspector.cs create mode 100644 tests/fixtures/reference-cache-v2/v2/Runtime/Export/Animation/Animator.bindings.cs create mode 100644 tests/fixtures/reference-cache-v2/v2/Runtime/Export/New/Awaitable.cs diff --git a/.claude-plugin/plugins/unity-cli/skills/unity-csharp-reference/SKILL.md b/.claude-plugin/plugins/unity-cli/skills/unity-csharp-reference/SKILL.md index dd7ed38..fbcf3de 100644 --- a/.claude-plugin/plugins/unity-cli/skills/unity-csharp-reference/SKILL.md +++ b/.claude-plugin/plugins/unity-cli/skills/unity-csharp-reference/SKILL.md @@ -49,11 +49,17 @@ unity-cli reference status --output json unity-cli reference find-symbol --name Animator --kind class unity-cli reference grep "class Animator " --context 3 unity-cli reference view Runtime/Export/Animation/Animator.bindings.cs --start-line 100 --max-lines 60 +unity-cli reference diff --from 2022.3.10f1 --to 2023.2.20f1 --symbol UnityEngine.Animator +unity-cli reference resolve-symbol-at Assets/Scripts/Player.cs --line 42 --column 18 unity-cli reference clean --keep 1 --dry-run ``` Use `reference find-symbol` first when you already know the type name (class / interface / struct / enum). It is backed by an on-disk index per Unity version (`~/.unity/cache/UnityCsReference//.unity-cli-index/symbols.json`) and is faster than `grep` for repeated lookups. Drop back to `grep` for free-text patterns or member-level matches. +Use `reference diff` to compare two cached Unity versions. The default symbol-only mode (`--symbol `) returns a single before/after pair, and the opt-in path mode (`--path [--max-symbols N]`) lists added / removed / changed type definitions in a directory. + +Use `reference resolve-symbol-at --line --column ` when the user is on a specific cursor position in a project file (`Assets/...` or `Packages/...`) and wants to see the canonical Unity reference for the type under the cursor. The tool is a thin wrapper: it reads the project file, extracts the identifier at the cursor, and feeds it through `reference find-symbol` + `reference view` for each cached version. + ## Examples - "Show me how Unity implements `Animator.Play` so I can predict whether it allocates." diff --git a/.claude-plugin/plugins/unity-cli/skills/unity-csharp-reference/references/version-diff-playbook.md b/.claude-plugin/plugins/unity-cli/skills/unity-csharp-reference/references/version-diff-playbook.md new file mode 100644 index 0000000..131faac --- /dev/null +++ b/.claude-plugin/plugins/unity-cli/skills/unity-csharp-reference/references/version-diff-playbook.md @@ -0,0 +1,96 @@ +# Version Diff Playbook + +Use this flow when comparing Unity API behavior across two cached versions, or when an LLM-suggested API needs to be validated against the canonical source for a specific project version. + +## When to use + +- The user wants to migrate a project from Unity X to Unity Y and asks which APIs changed. +- An LLM proposed `Animator.Play(stateName, layer)` but the project pins an older Unity version where that overload may not exist. +- A bug report cites a behavior change between Unity LTS versions and the team wants evidence from the source. + +## Prerequisites + +1. Both Unity versions are cached locally. Run `unity-cli reference status --output json` and check that the targets are listed. If not, fetch them: + + ```bash + unity-cli reference fetch --version 2022.3.10f1 --branch 2022.3/staging --accept-license + unity-cli reference fetch --version 2023.2.20f1 --branch 2023.2/staging --accept-license + ``` + +2. The Phase 2 symbol index is generated on the first `find-symbol` or `diff` call per version; no extra command is needed. + +## Symbol-only diff (default) + +```bash +unity-cli reference diff --from 2022.3.10f1 --to 2023.2.20f1 --symbol UnityEngine.Animator +``` + +Output shape (`diffs` is an array; empty if the symbol is missing in both versions): + +```json +{ + "ok": true, + "from": "2022.3.10f1", + "to": "2023.2.20f1", + "diffs": [ + { + "symbol": "UnityEngine.Animator", + "kind": "class", + "beforePath": "Runtime/Export/Animation/Animator.bindings.cs", + "beforeLine": 6, + "afterPath": "Runtime/Export/Animation/Animator.bindings.cs", + "afterLine": 8, + "hunks": [{ "before": ["..."], "after": ["..."], "beforeStart": 1, "afterStart": 1 }] + } + ] +} +``` + +Tips: + +- The view window is 30 lines around the symbol declaration. Use `reference view` for a larger window. +- When the symbol exists in only one version, the missing side has `beforePath` / `beforeLine` (or after) as `null`. Treat that as added / removed. + +## Path-range diff (opt-in) + +```bash +unity-cli reference diff --from 2022.3.10f1 --to 2023.2.20f1 --path Runtime/Export/Animation --max-symbols 50 +``` + +Returns `{added, removed, changed, truncated}`. Use this when scanning a subdirectory; raise `--max-symbols` cautiously since each `changed` entry triggers a view + line diff. `truncated: true` means the cap was hit and there may be more results. + +## Cursor-driven resolve + +```bash +unity-cli reference resolve-symbol-at Assets/Scripts/Player.cs --line 42 --column 18 --version 2023.2.20f1 +``` + +Output shape: + +```json +{ + "ok": true, + "cursorPath": "Assets/Scripts/Player.cs", + "cursorLine": 42, + "cursorColumn": 18, + "tokenName": "Animator", + "candidates": [ + { + "version": "2023.2.20f1", + "fqn": "UnityEngine.Animator", + "kind": "class", + "referencePath": "Runtime/Export/Animation/Animator.bindings.cs", + "referenceLine": 8, + "viewExcerpt": ["..."] + } + ] +} +``` + +The CLI extracts the identifier at the cursor (alphanumeric + `_`) and returns `null` when the cursor lands on whitespace, a comment, or a string literal. When `--version` is omitted, all cached versions are scanned and `candidates` is a flat list. The `unity-cli reference resolve-symbol-at` flow is a thin wrapper around `find-symbol` + `view`; it does not touch the csharp-lsp workspace. + +## Anti-patterns + +- Calling `reference diff --path` over the whole `Runtime/` tree without `--max-symbols`. The default cap is 50; the result still walks both versions' indexes once. +- Trusting `tokenName` when the cursor is inside a string literal or comment. The current extractor is regex-based and does not understand C# lexing. +- Using `resolve-symbol-at` to confirm method-level behavior. Phase 2 only indexes type definitions; for member-level signatures continue with `reference grep` until Phase 2.5 lands. diff --git a/docs/tools.md b/docs/tools.md index 0fab736..aa52318 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -249,7 +249,7 @@ unity-cli tool schema input_keyboard --output json unity-cli tool schema package_manager --output json ``` -## Reference Cache (7 tools) +## Reference Cache (9 tools) The `unity-cli reference *` family provides a local read-only mirror of the official [UnityCsReference](https://github.com/Unity-Technologies/UnityCsReference) @@ -268,6 +268,8 @@ fetch via `--accept-license` or `UNITY_CLI_ACCEPT_LICENSE=1`. | `reference_view` | Display a slice of a file in the cached reference source by line range. | | `reference_clean` | Remove old UnityCsReference snapshots, keeping the newest entries. | | `reference_find_symbol` | Look up type definitions (class / interface / struct / enum) in the cached reference source via a per-version on-disk index. | +| `reference_diff` | Compare a symbol or path range between two cached Unity versions. Returns symbol-level hunks or `{added, removed, changed}`. | +| `reference_resolve_symbol_at` | Resolve the identifier at a project cursor position (`Assets/...` / `Packages/...`) to candidate reference cache entries with view excerpts. | Typed CLI equivalents: @@ -277,6 +279,8 @@ unity-cli reference status --output json unity-cli reference find-symbol --name Animator --kind class unity-cli reference grep "class Animator " --context 3 unity-cli reference view Runtime/Export/Animation/Animator.bindings.cs --start-line 100 --max-lines 60 +unity-cli reference diff --from 2022.3.10f1 --to 2023.2.20f1 --symbol UnityEngine.Animator +unity-cli reference resolve-symbol-at Assets/Scripts/Player.cs --line 42 --column 18 unity-cli reference clean --keep 1 --dry-run ``` diff --git a/src/app/runner.rs b/src/app/runner.rs index ca475ea..9ddcfe2 100644 --- a/src/app/runner.rs +++ b/src/app/runner.rs @@ -345,6 +345,43 @@ fn build_reference_call(command: &ReferenceCommand) -> (&'static str, Value) { } return ("reference_find_symbol", Value::Object(params)); } + ReferenceCommand::Diff { + from, + to, + symbol, + path, + max_symbols, + } => { + params.insert("from".to_string(), Value::String(from.clone())); + params.insert("to".to_string(), Value::String(to.clone())); + if let Some(s) = symbol { + params.insert("symbol".to_string(), Value::String(s.clone())); + } + if let Some(p) = path { + params.insert("path".to_string(), Value::String(p.clone())); + } + if let Some(n) = max_symbols { + params.insert("maxSymbols".to_string(), Value::Number((*n).into())); + } + return ("reference_diff", Value::Object(params)); + } + ReferenceCommand::ResolveSymbolAt { + path, + line, + column, + version, + } => { + params.insert("path".to_string(), Value::String(path.clone())); + params.insert("line".to_string(), Value::Number((u64::from(*line)).into())); + params.insert( + "column".to_string(), + Value::Number((u64::from(*column)).into()), + ); + if let Some(v) = version { + params.insert("version".to_string(), Value::String(v.clone())); + } + return ("reference_resolve_symbol_at", Value::Object(params)); + } ReferenceCommand::Clean { keep, version, @@ -2160,6 +2197,69 @@ mod tests { assert_eq!(params["dryRun"], true); } + #[test] + fn build_reference_call_diff_symbol_mode() { + let cmd = ReferenceCommand::Diff { + from: "2022.3.10f1".to_string(), + to: "2023.2.20f1".to_string(), + symbol: Some("UnityEngine.Animator".to_string()), + path: None, + max_symbols: None, + }; + let (tool, params) = build_reference_call(&cmd); + assert_eq!(tool, "reference_diff"); + assert_eq!(params["from"], "2022.3.10f1"); + assert_eq!(params["to"], "2023.2.20f1"); + assert_eq!(params["symbol"], "UnityEngine.Animator"); + assert!(params.get("path").is_none()); + assert!(params.get("maxSymbols").is_none()); + } + + #[test] + fn build_reference_call_diff_path_mode_with_limit() { + let cmd = ReferenceCommand::Diff { + from: "v1".to_string(), + to: "v2".to_string(), + symbol: None, + path: Some("Runtime/Export".to_string()), + max_symbols: Some(20), + }; + let (tool, params) = build_reference_call(&cmd); + assert_eq!(tool, "reference_diff"); + assert_eq!(params["path"], "Runtime/Export"); + assert_eq!(params["maxSymbols"], 20); + assert!(params.get("symbol").is_none()); + } + + #[test] + fn build_reference_call_resolve_symbol_at_with_version() { + let cmd = ReferenceCommand::ResolveSymbolAt { + path: "Assets/Scripts/Player.cs".to_string(), + line: 42, + column: 18, + version: Some("2023.2.20f1".to_string()), + }; + let (tool, params) = build_reference_call(&cmd); + assert_eq!(tool, "reference_resolve_symbol_at"); + assert_eq!(params["path"], "Assets/Scripts/Player.cs"); + assert_eq!(params["line"], 42); + assert_eq!(params["column"], 18); + assert_eq!(params["version"], "2023.2.20f1"); + } + + #[test] + fn build_reference_call_resolve_symbol_at_minimal() { + let cmd = ReferenceCommand::ResolveSymbolAt { + path: "Packages/com.acme/Foo.cs".to_string(), + line: 1, + column: 1, + version: None, + }; + let (tool, params) = build_reference_call(&cmd); + assert_eq!(tool, "reference_resolve_symbol_at"); + assert!(params.get("version").is_none()); + } + #[test] fn build_reference_call_find_symbol_with_filters() { let cmd = ReferenceCommand::FindSymbol { diff --git a/src/cli.rs b/src/cli.rs index 5705c4c..df599d4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -282,6 +282,29 @@ pub enum ReferenceCommand { #[arg(long)] version: Option, }, + /// Diff a symbol or path range between two cached Unity versions. + Diff { + #[arg(long)] + from: String, + #[arg(long)] + to: String, + #[arg(long)] + symbol: Option, + #[arg(long)] + path: Option, + #[arg(long)] + max_symbols: Option, + }, + /// Resolve the C# token at a cursor position to candidate reference cache entries. + ResolveSymbolAt { + path: String, + #[arg(long)] + line: u32, + #[arg(long)] + column: u32, + #[arg(long)] + version: Option, + }, /// Remove old UnityCsReference snapshots, keeping the newest entries. Clean { #[arg(long, default_value_t = 1)] diff --git a/src/reference/diff.rs b/src/reference/diff.rs new file mode 100644 index 0000000..2dc1e95 --- /dev/null +++ b/src/reference/diff.rs @@ -0,0 +1,385 @@ +use std::path::Path; + +use anyhow::{anyhow, Context, Result}; +use serde::Serialize; + +use crate::reference::index; +use crate::reference::search; + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct Hunk { + pub before: Vec, + pub after: Vec, + #[serde(rename = "beforeStart")] + pub before_start: u32, + #[serde(rename = "afterStart")] + pub after_start: u32, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct SymbolDiff { + pub symbol: String, + pub kind: String, + #[serde( + rename = "beforePath", + skip_serializing_if = "Option::is_none", + default + )] + pub before_path: Option, + #[serde( + rename = "beforeLine", + skip_serializing_if = "Option::is_none", + default + )] + pub before_line: Option, + #[serde(rename = "afterPath", skip_serializing_if = "Option::is_none", default)] + pub after_path: Option, + #[serde(rename = "afterLine", skip_serializing_if = "Option::is_none", default)] + pub after_line: Option, + pub hunks: Vec, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct SymbolSummary { + pub symbol: String, + pub kind: String, + pub path: String, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq, Default)] +pub struct PathDiff { + pub added: Vec, + pub removed: Vec, + pub changed: Vec, + pub truncated: bool, +} + +const DEFAULT_VIEW_WINDOW: u32 = 30; +const DEFAULT_MAX_SYMBOLS: usize = 50; + +pub fn compute_line_diff(before: &[String], after: &[String]) -> Vec { + if before == after { + return vec![]; + } + vec![Hunk { + before: before.to_vec(), + after: after.to_vec(), + before_start: 1, + after_start: 1, + }] +} + +pub fn compute_symbol_diff( + from_dir: &Path, + to_dir: &Path, + symbol_fqn: &str, +) -> Result> { + let from_index = index::build_or_update_index(from_dir) + .with_context(|| format!("failed to index {}", from_dir.display()))?; + let to_index = index::build_or_update_index(to_dir) + .with_context(|| format!("failed to index {}", to_dir.display()))?; + let (name, namespace) = split_fqn(symbol_fqn); + let before_hit = find_first(&from_index, name, namespace.as_deref()); + let after_hit = find_first(&to_index, name, namespace.as_deref()); + + if before_hit.is_none() && after_hit.is_none() { + return Ok(None); + } + let before_lines = match &before_hit { + Some(hit) => read_excerpt(from_dir, &hit.path, hit.line)?, + None => Vec::new(), + }; + let after_lines = match &after_hit { + Some(hit) => read_excerpt(to_dir, &hit.path, hit.line)?, + None => Vec::new(), + }; + let kind = before_hit + .as_ref() + .or(after_hit.as_ref()) + .map(|h| h.kind.clone()) + .unwrap_or_default(); + let hunks = compute_line_diff(&before_lines, &after_lines); + Ok(Some(SymbolDiff { + symbol: symbol_fqn.to_string(), + kind, + before_path: before_hit.as_ref().map(|h| h.path.clone()), + before_line: before_hit.as_ref().map(|h| h.line), + after_path: after_hit.as_ref().map(|h| h.path.clone()), + after_line: after_hit.as_ref().map(|h| h.line), + hunks, + })) +} + +pub fn compute_path_diff( + from_dir: &Path, + to_dir: &Path, + path_filter: Option<&str>, + max_symbols: Option, +) -> Result { + let from_index = index::build_or_update_index(from_dir) + .with_context(|| format!("failed to index {}", from_dir.display()))?; + let to_index = index::build_or_update_index(to_dir) + .with_context(|| format!("failed to index {}", to_dir.display()))?; + let max = max_symbols.unwrap_or(DEFAULT_MAX_SYMBOLS); + + let mut before_map: std::collections::BTreeMap = + std::collections::BTreeMap::new(); + let mut after_map: std::collections::BTreeMap = + std::collections::BTreeMap::new(); + let prefix = path_filter.unwrap_or(""); + + for file in from_index.files.values() { + for sym in &file.symbols { + if !sym.path.starts_with(prefix) { + continue; + } + before_map.entry(symbol_key(sym)).or_insert(sym); + } + } + for file in to_index.files.values() { + for sym in &file.symbols { + if !sym.path.starts_with(prefix) { + continue; + } + after_map.entry(symbol_key(sym)).or_insert(sym); + } + } + + let mut added: Vec = Vec::new(); + let mut removed: Vec = Vec::new(); + let mut changed: Vec = Vec::new(); + let mut total = 0usize; + let mut truncated = false; + + for (key, sym) in &after_map { + if !before_map.contains_key(key) { + if total >= max { + truncated = true; + break; + } + added.push(SymbolSummary { + symbol: key.clone(), + kind: sym.kind.clone(), + path: sym.path.clone(), + }); + total += 1; + } + } + if !truncated { + for (key, sym) in &before_map { + if !after_map.contains_key(key) { + if total >= max { + truncated = true; + break; + } + removed.push(SymbolSummary { + symbol: key.clone(), + kind: sym.kind.clone(), + path: sym.path.clone(), + }); + total += 1; + } + } + } + if !truncated { + for (key, before_sym) in &before_map { + if let Some(after_sym) = after_map.get(key) { + if total >= max { + truncated = true; + break; + } + let before_lines = read_excerpt(from_dir, &before_sym.path, before_sym.line)?; + let after_lines = read_excerpt(to_dir, &after_sym.path, after_sym.line)?; + let hunks = compute_line_diff(&before_lines, &after_lines); + if hunks.is_empty() { + continue; + } + changed.push(SymbolDiff { + symbol: key.clone(), + kind: before_sym.kind.clone(), + before_path: Some(before_sym.path.clone()), + before_line: Some(before_sym.line), + after_path: Some(after_sym.path.clone()), + after_line: Some(after_sym.line), + hunks, + }); + total += 1; + } + } + } + + Ok(PathDiff { + added, + removed, + changed, + truncated, + }) +} + +fn split_fqn(fqn: &str) -> (&str, Option) { + if let Some(idx) = fqn.rfind('.') { + let (ns, name) = fqn.split_at(idx); + (&name[1..], Some(ns.to_string())) + } else { + (fqn, None) + } +} + +fn find_first( + index: &index::ReferenceSymbolIndex, + name: &str, + namespace: Option<&str>, +) -> Option { + index::find_symbol(index, name, None, namespace) + .into_iter() + .next() +} + +fn symbol_key(sym: &index::ReferenceSymbolEntry) -> String { + sym.fqn.clone().unwrap_or_else(|| sym.name.clone()) +} + +fn read_excerpt(root: &Path, rel_path: &str, line: u32) -> Result> { + let start = line.saturating_sub(1).max(1); + let view = search::run_view(root, rel_path, Some(start), Some(DEFAULT_VIEW_WINDOW)) + .with_context(|| format!("failed to read excerpt for {rel_path} at line {line}"))?; + Ok(view.lines) +} + +pub fn ensure_cache_dir(dir: &Path, label: &str) -> Result<()> { + if !dir.exists() { + return Err(anyhow!( + "reference cache for {} ({}) does not exist; run `unity-cli reference fetch --version {}` first", + label, + dir.display(), + label + )); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + fn fixture_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/reference-cache-v2") + } + + fn v1() -> PathBuf { + fixture_root().join("v1") + } + + fn v2() -> PathBuf { + fixture_root().join("v2") + } + + #[test] + fn compute_line_diff_empty_for_identical_lines() { + let lines = vec!["a".to_string(), "b".to_string()]; + assert!(compute_line_diff(&lines, &lines).is_empty()); + } + + #[test] + fn compute_line_diff_produces_single_hunk_for_changes() { + let before = vec!["a".to_string(), "b".to_string()]; + let after = vec!["a".to_string(), "c".to_string()]; + let hunks = compute_line_diff(&before, &after); + assert_eq!(hunks.len(), 1); + assert_eq!(hunks[0].before, before); + assert_eq!(hunks[0].after, after); + assert_eq!(hunks[0].before_start, 1); + assert_eq!(hunks[0].after_start, 1); + } + + #[test] + fn split_fqn_separates_namespace_and_name() { + assert_eq!( + split_fqn("UnityEngine.Animator"), + ("Animator", Some("UnityEngine".to_string())) + ); + assert_eq!(split_fqn("Animator"), ("Animator", None)); + assert_eq!( + split_fqn("UnityEngine.Animation.Animator"), + ("Animator", Some("UnityEngine.Animation".to_string())) + ); + } + + #[test] + fn compute_symbol_diff_detects_changed_animator() { + let diff = compute_symbol_diff(&v1(), &v2(), "UnityEngine.Animator") + .unwrap() + .expect("Animator should be present in both versions"); + assert_eq!(diff.symbol, "UnityEngine.Animator"); + assert_eq!(diff.kind, "class"); + assert!(diff.before_path.is_some()); + assert!(diff.after_path.is_some()); + assert!( + !diff.hunks.is_empty(), + "Animator changed in v2 should yield hunks" + ); + } + + #[test] + fn compute_symbol_diff_returns_none_for_missing_in_both() { + let result = compute_symbol_diff(&v1(), &v2(), "UnityEngine.DefinitelyNotHere").unwrap(); + assert!(result.is_none()); + } + + #[test] + fn compute_symbol_diff_returns_diff_when_only_one_side_has_symbol() { + let added_only = compute_symbol_diff(&v1(), &v2(), "UnityEngine.Awaitable") + .unwrap() + .expect("Awaitable should be present in v2"); + assert!(added_only.before_path.is_none()); + assert!(added_only.after_path.is_some()); + let removed_only = compute_symbol_diff(&v1(), &v2(), "UnityEngine.LegacyAnimator") + .unwrap() + .expect("LegacyAnimator should be present in v1"); + assert!(removed_only.before_path.is_some()); + assert!(removed_only.after_path.is_none()); + } + + #[test] + fn compute_path_diff_categorises_runtime_export() { + let diff = compute_path_diff(&v1(), &v2(), Some("Runtime/Export"), None).unwrap(); + assert!( + diff.added.iter().any(|s| s.symbol.ends_with("Awaitable")), + "Awaitable should be added: {:?}", + diff.added + ); + assert!( + diff.removed + .iter() + .any(|s| s.symbol.ends_with("LegacyAnimator")), + "LegacyAnimator should be removed: {:?}", + diff.removed + ); + assert!( + diff.changed + .iter() + .any(|s| s.symbol.ends_with("Animator") && !s.symbol.ends_with("LegacyAnimator")), + "Animator should be changed: {:?}", + diff.changed + ); + assert!(!diff.truncated); + } + + #[test] + fn compute_path_diff_can_be_truncated() { + let diff = compute_path_diff(&v1(), &v2(), None, Some(1)).unwrap(); + let total = diff.added.len() + diff.removed.len() + diff.changed.len(); + assert_eq!(total, 1); + assert!(diff.truncated); + } + + #[test] + fn ensure_cache_dir_errors_on_missing() { + let tmp = tempfile::TempDir::new().unwrap(); + let err = ensure_cache_dir(&tmp.path().join("missing"), "2023.2.20f1").unwrap_err(); + let msg = format!("{err:#}"); + assert!(msg.contains("does not exist")); + assert!(msg.contains("2023.2.20f1")); + } +} diff --git a/src/reference/mod.rs b/src/reference/mod.rs index 2679126..61ac6f0 100644 --- a/src/reference/mod.rs +++ b/src/reference/mod.rs @@ -1,6 +1,7 @@ #![allow(dead_code)] pub mod cache; +pub mod diff; pub mod fetcher; pub mod index; pub mod search; @@ -22,6 +23,8 @@ pub fn maybe_execute_reference_tool(tool_name: &str, params: &Value) -> Option Some(execute_view(params)), "reference_clean" => Some(execute_clean(params)), "reference_find_symbol" => Some(execute_find_symbol(params)), + "reference_diff" => Some(execute_diff(params)), + "reference_resolve_symbol_at" => Some(execute_resolve_symbol_at(params)), _ => None, } } @@ -227,6 +230,169 @@ fn execute_find_symbol(params: &Value) -> Result { })) } +fn execute_diff(params: &Value) -> Result { + let from = params + .get("from") + .and_then(Value::as_str) + .filter(|s| !s.is_empty()) + .ok_or_else(|| anyhow!("diff requires `from`"))?; + let to = params + .get("to") + .and_then(Value::as_str) + .filter(|s| !s.is_empty()) + .ok_or_else(|| anyhow!("diff requires `to`"))?; + let symbol = params + .get("symbol") + .and_then(Value::as_str) + .filter(|s| !s.is_empty()); + let path_filter = params + .get("path") + .and_then(Value::as_str) + .filter(|s| !s.is_empty()); + let max_symbols = params + .get("maxSymbols") + .and_then(Value::as_u64) + .map(|n| n as usize); + if symbol.is_none() && path_filter.is_none() { + return Err(anyhow!("diff requires either `symbol` or `path`")); + } + let from_dir = cache::version_dir(from)?; + let to_dir = cache::version_dir(to)?; + diff::ensure_cache_dir(&from_dir, from)?; + diff::ensure_cache_dir(&to_dir, to)?; + + if let Some(fqn) = symbol { + let diff_value = diff::compute_symbol_diff(&from_dir, &to_dir, fqn)?; + return Ok(json!({ + "ok": true, + "from": from, + "to": to, + "diffs": diff_value.map(|d| vec![d]).unwrap_or_default(), + })); + } + let path_diff = diff::compute_path_diff(&from_dir, &to_dir, path_filter, max_symbols)?; + Ok(json!({ + "ok": true, + "from": from, + "to": to, + "path": path_filter, + "added": path_diff.added, + "removed": path_diff.removed, + "changed": path_diff.changed, + "truncated": path_diff.truncated, + })) +} + +fn execute_resolve_symbol_at(params: &Value) -> Result { + let path = params + .get("path") + .and_then(Value::as_str) + .ok_or_else(|| anyhow!("resolve_symbol_at requires `path`"))?; + let line = params + .get("line") + .and_then(Value::as_u64) + .ok_or_else(|| anyhow!("resolve_symbol_at requires `line`"))? as u32; + let column = params + .get("column") + .and_then(Value::as_u64) + .ok_or_else(|| anyhow!("resolve_symbol_at requires `column`"))? as u32; + let version_hint = params + .get("version") + .and_then(Value::as_str) + .filter(|s| !s.is_empty()); + let project_root = params + .get("projectRoot") + .and_then(Value::as_str) + .map(std::path::Path::new); + + if !path.starts_with("Assets/") && !path.starts_with("Packages/") { + return Err(anyhow!( + "project path must start with Assets/ or Packages/: {path}" + )); + } + + let abs_path = match project_root { + Some(root) => root.join(path), + None => std::path::PathBuf::from(path), + }; + let contents = std::fs::read_to_string(&abs_path) + .with_context(|| format!("failed to read project file {}", abs_path.display()))?; + let token = extract_token_at_cursor(&contents, line, column); + let candidates = match &token { + Some(name) => collect_resolve_candidates(name, version_hint)?, + None => Vec::new(), + }; + Ok(json!({ + "ok": true, + "cursorPath": path, + "cursorLine": line, + "cursorColumn": column, + "tokenName": token, + "candidates": candidates, + })) +} + +fn extract_token_at_cursor(content: &str, line: u32, column: u32) -> Option { + let lines: Vec<&str> = content.lines().collect(); + let line_idx = line.saturating_sub(1) as usize; + let row = lines.get(line_idx)?; + let chars: Vec = row.chars().collect(); + let col_idx = column.saturating_sub(1) as usize; + if col_idx >= chars.len() || !is_ident_char(chars[col_idx]) { + return None; + } + let mut start = col_idx; + while start > 0 && is_ident_char(chars[start - 1]) { + start -= 1; + } + let mut end = col_idx + 1; + while end < chars.len() && is_ident_char(chars[end]) { + end += 1; + } + Some(chars[start..end].iter().collect()) +} + +fn is_ident_char(c: char) -> bool { + c.is_alphanumeric() || c == '_' +} + +const DEFAULT_RESOLVE_VIEW_WINDOW: u32 = 30; + +fn collect_resolve_candidates(name: &str, version_hint: Option<&str>) -> Result> { + let versions: Vec = match version_hint { + Some(v) => vec![v.to_string()], + None => cache::list_versions().unwrap_or_default(), + }; + let mut candidates = Vec::new(); + for ver in &versions { + let dir = cache::version_dir(ver)?; + if !dir.exists() { + continue; + } + let symbol_index = index::build_or_update_index(&dir)?; + let hits = index::find_symbol(&symbol_index, name, None, None); + for hit in hits { + let view_excerpt = search::run_view( + &dir, + &hit.path, + Some(hit.line), + Some(DEFAULT_RESOLVE_VIEW_WINDOW), + ) + .map(|v| v.lines) + .unwrap_or_default(); + candidates.push(json!({ + "version": ver, + "fqn": hit.fqn.clone().unwrap_or_else(|| hit.name.clone()), + "kind": hit.kind, + "referencePath": hit.path, + "referenceLine": hit.line, + "viewExcerpt": view_excerpt, + })); + } + } + Ok(candidates) +} + fn execute_clean(params: &Value) -> Result { let keep = params.get("keep").and_then(Value::as_u64).unwrap_or(1) as usize; let dry_run = params @@ -797,4 +963,219 @@ mod tests { assert!(value["hits"].as_array().unwrap().is_empty()); let _ = std::fs::remove_dir_all(&root); } + + fn setup_two_version_cache(label: &str) -> (PathBuf, &'static str, &'static str) { + let root = unique_temp_path(label); + let base = root.join("UnityCsReference"); + let fixture_v1 = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/reference-cache-v2/v1"); + let fixture_v2 = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/reference-cache-v2/v2"); + copy_dir_recursive(&fixture_v1, &base.join("v1")).unwrap(); + copy_dir_recursive(&fixture_v2, &base.join("v2")).unwrap(); + (root, "v1", "v2") + } + + #[test] + fn execute_diff_requires_from_and_to() { + let err1 = maybe_execute_reference_tool("reference_diff", &json!({"to": "v2"})) + .unwrap() + .unwrap_err(); + assert!(format!("{err1:#}").contains("from")); + let err2 = maybe_execute_reference_tool("reference_diff", &json!({"from": "v1"})) + .unwrap() + .unwrap_err(); + assert!(format!("{err2:#}").contains("to")); + } + + #[test] + fn execute_diff_requires_symbol_or_path() { + let _guard = crate::test_env::env_lock() + .lock() + .unwrap_or_else(|p| p.into_inner()); + let (root, from, to) = setup_two_version_cache("diff-need"); + let _env = EnvVarGuard::set("UNITY_CLI_CACHE_ROOT", root.to_str().unwrap()); + let err = maybe_execute_reference_tool("reference_diff", &json!({"from": from, "to": to})) + .unwrap() + .unwrap_err(); + assert!(format!("{err:#}").contains("symbol")); + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn execute_diff_symbol_mode_returns_diffs() { + let _guard = crate::test_env::env_lock() + .lock() + .unwrap_or_else(|p| p.into_inner()); + let (root, from, to) = setup_two_version_cache("diff-sym"); + let _env = EnvVarGuard::set("UNITY_CLI_CACHE_ROOT", root.to_str().unwrap()); + let value = maybe_execute_reference_tool( + "reference_diff", + &json!({"from": from, "to": to, "symbol": "UnityEngine.Animator"}), + ) + .unwrap() + .unwrap(); + assert_eq!(value["ok"], true); + let diffs = value["diffs"].as_array().unwrap(); + assert!(!diffs.is_empty()); + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn execute_diff_path_mode_returns_added_removed_changed() { + let _guard = crate::test_env::env_lock() + .lock() + .unwrap_or_else(|p| p.into_inner()); + let (root, from, to) = setup_two_version_cache("diff-path"); + let _env = EnvVarGuard::set("UNITY_CLI_CACHE_ROOT", root.to_str().unwrap()); + let value = maybe_execute_reference_tool( + "reference_diff", + &json!({"from": from, "to": to, "path": "Runtime/Export"}), + ) + .unwrap() + .unwrap(); + assert_eq!(value["ok"], true); + let added = value["added"].as_array().unwrap(); + let removed = value["removed"].as_array().unwrap(); + let changed = value["changed"].as_array().unwrap(); + assert!(added + .iter() + .any(|s| s["symbol"].as_str().unwrap_or("").ends_with("Awaitable"))); + assert!(removed.iter().any(|s| s["symbol"] + .as_str() + .unwrap_or("") + .ends_with("LegacyAnimator"))); + assert!(!changed.is_empty()); + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn execute_diff_errors_when_cache_missing() { + let _guard = crate::test_env::env_lock() + .lock() + .unwrap_or_else(|p| p.into_inner()); + let root = unique_temp_path("diff-miss"); + let _env = EnvVarGuard::set("UNITY_CLI_CACHE_ROOT", root.to_str().unwrap()); + let err = maybe_execute_reference_tool( + "reference_diff", + &json!({"from": "missing-v1", "to": "missing-v2", "symbol": "Foo"}), + ) + .unwrap() + .unwrap_err(); + assert!(format!("{err:#}").contains("does not exist")); + } + + #[test] + fn execute_resolve_symbol_at_requires_project_prefix() { + let err = maybe_execute_reference_tool( + "reference_resolve_symbol_at", + &json!({"path": "/tmp/bad.cs", "line": 1, "column": 1}), + ) + .unwrap() + .unwrap_err(); + assert!(format!("{err:#}").contains("Assets/")); + } + + #[test] + fn execute_resolve_symbol_at_requires_path_line_column() { + let e1 = maybe_execute_reference_tool( + "reference_resolve_symbol_at", + &json!({"line": 1, "column": 1}), + ) + .unwrap() + .unwrap_err(); + assert!(format!("{e1:#}").contains("path")); + let e2 = maybe_execute_reference_tool( + "reference_resolve_symbol_at", + &json!({"path": "Assets/X.cs", "column": 1}), + ) + .unwrap() + .unwrap_err(); + assert!(format!("{e2:#}").contains("line")); + let e3 = maybe_execute_reference_tool( + "reference_resolve_symbol_at", + &json!({"path": "Assets/X.cs", "line": 1}), + ) + .unwrap() + .unwrap_err(); + assert!(format!("{e3:#}").contains("column")); + } + + #[test] + fn execute_resolve_symbol_at_finds_token_via_fixture() { + let _guard = crate::test_env::env_lock() + .lock() + .unwrap_or_else(|p| p.into_inner()); + let (root, _from, to) = setup_two_version_cache("resolve-token"); + let _env = EnvVarGuard::set("UNITY_CLI_CACHE_ROOT", root.to_str().unwrap()); + // Create a fake project file + let project_root = unique_temp_path("resolve-project"); + std::fs::create_dir_all(project_root.join("Assets/Scripts")).unwrap(); + std::fs::write( + project_root.join("Assets/Scripts/Player.cs"), + "public class Player {\n void Update() {\n var a = new Animator();\n }\n}\n", + ) + .unwrap(); + let value = maybe_execute_reference_tool( + "reference_resolve_symbol_at", + &json!({ + "path": "Assets/Scripts/Player.cs", + "line": 3, + "column": 21, + "projectRoot": project_root.to_str().unwrap(), + "version": to, + }), + ) + .unwrap() + .unwrap(); + assert_eq!(value["tokenName"], "Animator"); + let candidates = value["candidates"].as_array().unwrap(); + assert!( + !candidates.is_empty(), + "Animator should resolve to v2 candidate" + ); + let _ = std::fs::remove_dir_all(&root); + let _ = std::fs::remove_dir_all(&project_root); + } + + #[test] + fn execute_resolve_symbol_at_empty_token_for_blank_position() { + let _guard = crate::test_env::env_lock() + .lock() + .unwrap_or_else(|p| p.into_inner()); + let project_root = unique_temp_path("resolve-blank"); + std::fs::create_dir_all(project_root.join("Assets")).unwrap(); + std::fs::write(project_root.join("Assets/Foo.cs"), "// comment line\n").unwrap(); + let value = maybe_execute_reference_tool( + "reference_resolve_symbol_at", + &json!({ + "path": "Assets/Foo.cs", + "line": 1, + "column": 1, + "projectRoot": project_root.to_str().unwrap(), + }), + ) + .unwrap() + .unwrap(); + assert!(value["tokenName"].is_null() || value["tokenName"] == ""); + assert!(value["candidates"].as_array().unwrap().is_empty()); + let _ = std::fs::remove_dir_all(&project_root); + } + + #[test] + fn extract_token_at_cursor_handles_edge_cases() { + let content = "var animator = new Animator();\n"; + assert_eq!( + extract_token_at_cursor(content, 1, 5), + Some("animator".to_string()) + ); + assert_eq!( + extract_token_at_cursor(content, 1, 20), + Some("Animator".to_string()) + ); + // Whitespace position returns None + assert_eq!(extract_token_at_cursor(content, 1, 4), None); + // Out-of-range line returns None + assert_eq!(extract_token_at_cursor(content, 99, 1), None); + } } diff --git a/src/tooling/tool_catalog.rs b/src/tooling/tool_catalog.rs index 4051b2e..f45ecc0 100644 --- a/src/tooling/tool_catalog.rs +++ b/src/tooling/tool_catalog.rs @@ -127,6 +127,8 @@ pub const TOOL_NAMES: &[&str] = &[ "reference_view", "reference_clean", "reference_find_symbol", + "reference_diff", + "reference_resolve_symbol_at", ]; #[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] @@ -230,7 +232,9 @@ fn tool_executor(name: &str) -> ToolExecutor { | "reference_grep" | "reference_view" | "reference_clean" - | "reference_find_symbol" => ToolExecutor::Local, + | "reference_find_symbol" + | "reference_diff" + | "reference_resolve_symbol_at" => ToolExecutor::Local, _ => ToolExecutor::Remote, } } @@ -283,6 +287,8 @@ fn is_read_only_tool(name: &str) -> bool { | "reference_grep" | "reference_view" | "reference_find_symbol" + | "reference_diff" + | "reference_resolve_symbol_at" ) } @@ -2306,6 +2312,28 @@ fn tool_params_schema(name: &str) -> Value { &["name"], false, ), + "reference_diff" => object_schema( + &[ + ("from", string_schema()), + ("to", string_schema()), + ("symbol", string_schema()), + ("path", string_schema()), + ("maxSymbols", integer_schema()), + ], + &["from", "to"], + false, + ), + "reference_resolve_symbol_at" => object_schema( + &[ + ("path", string_schema()), + ("line", integer_schema()), + ("column", integer_schema()), + ("version", string_schema()), + ("projectRoot", string_schema()), + ], + &["path", "line", "column"], + false, + ), _ => default_params_schema(), } } @@ -2420,7 +2448,7 @@ mod tests { #[test] fn tool_catalog_keeps_manifest_parity_count() { - assert_eq!(TOOL_NAMES.len(), 125); + assert_eq!(TOOL_NAMES.len(), 127); } #[test] diff --git a/tests/fixtures/reference-cache-v2/v1/.unity-cli-index/symbols.json b/tests/fixtures/reference-cache-v2/v1/.unity-cli-index/symbols.json new file mode 100644 index 0000000..2580869 --- /dev/null +++ b/tests/fixtures/reference-cache-v2/v1/.unity-cli-index/symbols.json @@ -0,0 +1,45 @@ +{ + "version": 1, + "generated_at_epoch_ms": 1778510932256, + "files": { + "Editor/AnimatorInspector.cs": { + "signature": "130:1778510638701", + "symbols": [ + { + "path": "Editor/AnimatorInspector.cs", + "name": "AnimatorInspector", + "kind": "class", + "line": 3, + "namespace": "UnityEditor", + "fqn": "UnityEditor.AnimatorInspector" + } + ] + }, + "Runtime/Export/Animation/Animator.bindings.cs": { + "signature": "139:1778510638483", + "symbols": [ + { + "path": "Runtime/Export/Animation/Animator.bindings.cs", + "name": "Animator", + "kind": "class", + "line": 3, + "namespace": "UnityEngine", + "fqn": "UnityEngine.Animator" + } + ] + }, + "Runtime/Export/Animation/Removed.cs": { + "signature": "70:1778510638916", + "symbols": [ + { + "path": "Runtime/Export/Animation/Removed.cs", + "name": "LegacyAnimator", + "kind": "class", + "line": 3, + "namespace": "UnityEngine", + "fqn": "UnityEngine.LegacyAnimator" + } + ] + } + } +} \ No newline at end of file diff --git a/tests/fixtures/reference-cache-v2/v1/Editor/AnimatorInspector.cs b/tests/fixtures/reference-cache-v2/v1/Editor/AnimatorInspector.cs new file mode 100644 index 0000000..e9a6c69 --- /dev/null +++ b/tests/fixtures/reference-cache-v2/v1/Editor/AnimatorInspector.cs @@ -0,0 +1,9 @@ +namespace UnityEditor +{ + public class AnimatorInspector + { + public void OnInspectorGUI() + { + } + } +} diff --git a/tests/fixtures/reference-cache-v2/v1/Runtime/Export/Animation/Animator.bindings.cs b/tests/fixtures/reference-cache-v2/v1/Runtime/Export/Animation/Animator.bindings.cs new file mode 100644 index 0000000..d05c4f0 --- /dev/null +++ b/tests/fixtures/reference-cache-v2/v1/Runtime/Export/Animation/Animator.bindings.cs @@ -0,0 +1,9 @@ +namespace UnityEngine +{ + public class Animator : Behaviour + { + public void Play(string stateName) + { + } + } +} diff --git a/tests/fixtures/reference-cache-v2/v1/Runtime/Export/Animation/Removed.cs b/tests/fixtures/reference-cache-v2/v1/Runtime/Export/Animation/Removed.cs new file mode 100644 index 0000000..76a0f4c --- /dev/null +++ b/tests/fixtures/reference-cache-v2/v1/Runtime/Export/Animation/Removed.cs @@ -0,0 +1,6 @@ +namespace UnityEngine +{ + public class LegacyAnimator + { + } +} diff --git a/tests/fixtures/reference-cache-v2/v2/.unity-cli-index/symbols.json b/tests/fixtures/reference-cache-v2/v2/.unity-cli-index/symbols.json new file mode 100644 index 0000000..0c2b99c --- /dev/null +++ b/tests/fixtures/reference-cache-v2/v2/.unity-cli-index/symbols.json @@ -0,0 +1,45 @@ +{ + "version": 1, + "generated_at_epoch_ms": 1778510932257, + "files": { + "Editor/AnimatorInspector.cs": { + "signature": "130:1778510639309", + "symbols": [ + { + "path": "Editor/AnimatorInspector.cs", + "name": "AnimatorInspector", + "kind": "class", + "line": 3, + "namespace": "UnityEditor", + "fqn": "UnityEditor.AnimatorInspector" + } + ] + }, + "Runtime/Export/Animation/Animator.bindings.cs": { + "signature": "214:1778510639108", + "symbols": [ + { + "path": "Runtime/Export/Animation/Animator.bindings.cs", + "name": "Animator", + "kind": "class", + "line": 3, + "namespace": "UnityEngine", + "fqn": "UnityEngine.Animator" + } + ] + }, + "Runtime/Export/New/Awaitable.cs": { + "signature": "145:1778510639498", + "symbols": [ + { + "path": "Runtime/Export/New/Awaitable.cs", + "name": "Awaitable", + "kind": "class", + "line": 3, + "namespace": "UnityEngine", + "fqn": "UnityEngine.Awaitable" + } + ] + } + } +} \ No newline at end of file diff --git a/tests/fixtures/reference-cache-v2/v2/Editor/AnimatorInspector.cs b/tests/fixtures/reference-cache-v2/v2/Editor/AnimatorInspector.cs new file mode 100644 index 0000000..e9a6c69 --- /dev/null +++ b/tests/fixtures/reference-cache-v2/v2/Editor/AnimatorInspector.cs @@ -0,0 +1,9 @@ +namespace UnityEditor +{ + public class AnimatorInspector + { + public void OnInspectorGUI() + { + } + } +} diff --git a/tests/fixtures/reference-cache-v2/v2/Runtime/Export/Animation/Animator.bindings.cs b/tests/fixtures/reference-cache-v2/v2/Runtime/Export/Animation/Animator.bindings.cs new file mode 100644 index 0000000..002ddf1 --- /dev/null +++ b/tests/fixtures/reference-cache-v2/v2/Runtime/Export/Animation/Animator.bindings.cs @@ -0,0 +1,13 @@ +namespace UnityEngine +{ + public class Animator : Behaviour + { + public void Play(string stateName) + { + } + + public void Play(string stateName, int layer) + { + } + } +} diff --git a/tests/fixtures/reference-cache-v2/v2/Runtime/Export/New/Awaitable.cs b/tests/fixtures/reference-cache-v2/v2/Runtime/Export/New/Awaitable.cs new file mode 100644 index 0000000..8c5a12b --- /dev/null +++ b/tests/fixtures/reference-cache-v2/v2/Runtime/Export/New/Awaitable.cs @@ -0,0 +1,10 @@ +namespace UnityEngine +{ + public class Awaitable + { + public bool IsCompleted() + { + return false; + } + } +} From b352aea02e5099535dcfba21d16ca786ee0e5203 Mon Sep 17 00:00:00 2001 From: Akio Jinsenji Date: Mon, 11 May 2026 23:51:57 +0900 Subject: [PATCH 2/3] =?UTF-8?q?chore(reference):=20test=20=E6=AE=8B?= =?UTF-8?q?=E9=AA=B8=E3=81=AE=20.unity-cli-index/=20=E3=82=92=20gitignore?= =?UTF-8?q?=20=E3=81=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 PR #190 で fixture 配下に build_or_update_index の生成物 (.unity-cli-index/symbols.json) を誤って commit してしまっていた。 test 実行ごとに mtime + signature が変動して diff を生むため、本 ディレクトリは fixture 配下では追跡しない方針に変更する。 Refs #188 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../v1/.unity-cli-index/symbols.json | 45 ------------------- .../v2/.unity-cli-index/symbols.json | 45 ------------------- 2 files changed, 90 deletions(-) delete mode 100644 tests/fixtures/reference-cache-v2/v1/.unity-cli-index/symbols.json delete mode 100644 tests/fixtures/reference-cache-v2/v2/.unity-cli-index/symbols.json diff --git a/tests/fixtures/reference-cache-v2/v1/.unity-cli-index/symbols.json b/tests/fixtures/reference-cache-v2/v1/.unity-cli-index/symbols.json deleted file mode 100644 index 2580869..0000000 --- a/tests/fixtures/reference-cache-v2/v1/.unity-cli-index/symbols.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "version": 1, - "generated_at_epoch_ms": 1778510932256, - "files": { - "Editor/AnimatorInspector.cs": { - "signature": "130:1778510638701", - "symbols": [ - { - "path": "Editor/AnimatorInspector.cs", - "name": "AnimatorInspector", - "kind": "class", - "line": 3, - "namespace": "UnityEditor", - "fqn": "UnityEditor.AnimatorInspector" - } - ] - }, - "Runtime/Export/Animation/Animator.bindings.cs": { - "signature": "139:1778510638483", - "symbols": [ - { - "path": "Runtime/Export/Animation/Animator.bindings.cs", - "name": "Animator", - "kind": "class", - "line": 3, - "namespace": "UnityEngine", - "fqn": "UnityEngine.Animator" - } - ] - }, - "Runtime/Export/Animation/Removed.cs": { - "signature": "70:1778510638916", - "symbols": [ - { - "path": "Runtime/Export/Animation/Removed.cs", - "name": "LegacyAnimator", - "kind": "class", - "line": 3, - "namespace": "UnityEngine", - "fqn": "UnityEngine.LegacyAnimator" - } - ] - } - } -} \ No newline at end of file diff --git a/tests/fixtures/reference-cache-v2/v2/.unity-cli-index/symbols.json b/tests/fixtures/reference-cache-v2/v2/.unity-cli-index/symbols.json deleted file mode 100644 index 0c2b99c..0000000 --- a/tests/fixtures/reference-cache-v2/v2/.unity-cli-index/symbols.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "version": 1, - "generated_at_epoch_ms": 1778510932257, - "files": { - "Editor/AnimatorInspector.cs": { - "signature": "130:1778510639309", - "symbols": [ - { - "path": "Editor/AnimatorInspector.cs", - "name": "AnimatorInspector", - "kind": "class", - "line": 3, - "namespace": "UnityEditor", - "fqn": "UnityEditor.AnimatorInspector" - } - ] - }, - "Runtime/Export/Animation/Animator.bindings.cs": { - "signature": "214:1778510639108", - "symbols": [ - { - "path": "Runtime/Export/Animation/Animator.bindings.cs", - "name": "Animator", - "kind": "class", - "line": 3, - "namespace": "UnityEngine", - "fqn": "UnityEngine.Animator" - } - ] - }, - "Runtime/Export/New/Awaitable.cs": { - "signature": "145:1778510639498", - "symbols": [ - { - "path": "Runtime/Export/New/Awaitable.cs", - "name": "Awaitable", - "kind": "class", - "line": 3, - "namespace": "UnityEngine", - "fqn": "UnityEngine.Awaitable" - } - ] - } - } -} \ No newline at end of file From 77d90671e33093f60ceceeeebcc301c20bf63c3d Mon Sep 17 00:00:00 2001 From: Akio Jinsenji Date: Mon, 11 May 2026 23:52:20 +0900 Subject: [PATCH 3/3] chore: ignore tests/fixtures/**/.unity-cli-index/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build_or_update_index が fixture 配下で実行されたときの生成物を追跡 対象から外す。再発防止として gitignore を追加。 Refs #188 Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 83 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index 52aec1c..8c86762 100644 --- a/.gitignore +++ b/.gitignore @@ -11,9 +11,9 @@ *.pdb.meta *.mdb.meta -# Unity meta files -# Don't ignore meta files in the Unity package -!UnityCliBridge/Packages/unity-cli-bridge/**/*.meta +# Unity meta files +# Don't ignore meta files in the Unity package +!UnityCliBridge/Packages/unity-cli-bridge/**/*.meta # Visual Studio cache directory .vs/ @@ -21,10 +21,10 @@ # Autogenerated VS/MD/Consulo solution and project files ExportedObj/ .consulo/ -*.csproj -!lsp/*.csproj -!lsp/Server.Core/*.csproj -!roslyn-cli/*.csproj +*.csproj +!lsp/*.csproj +!lsp/Server.Core/*.csproj +!roslyn-cli/*.csproj *.unityproj *.sln *.suo @@ -84,45 +84,48 @@ ehthumbs_vista.db # Test output coverage/ .nyc_output/ -tests/.reports/ -tests/.todo/ -tasks/todo.md - -# Build output -dist/ -build/ -target/ -.cache/ -CLAUDE.local.md - -.unity/ -UnityCliBridge/.unity/ -.mcp/ -UnityCliBridge/.mcp/ -.worktrees/ +tests/.reports/ +tests/.todo/ +tasks/todo.md + +# Build output +dist/ +build/ +target/ +.cache/ +CLAUDE.local.md + +.unity/ +UnityCliBridge/.unity/ +.mcp/ +UnityCliBridge/.mcp/ +.worktrees/ *.local.json -lsp/bin/ -lsp/out/ -lsp/Server.Core/bin/ -lsp/Server.Core/obj/ -lsp/coverage.cobertura.xml +lsp/bin/ +lsp/out/ +lsp/Server.Core/bin/ +lsp/Server.Core/obj/ +lsp/coverage.cobertura.xml # Claude Code Hook temporary files .claude/.last-suggest-timestamp .claude/.last-session-id -# Generated test files for code index scalability testing -UnityCliBridge/Assets/Scripts/Generated/ -UnityCliBridge/Assets/Scripts/Generated.meta - -# Generated Unity scenes -UnityCliBridge/Assets/Scenes/Generated/ -UnityCliBridge/Assets/Scenes/Generated.meta - -UnityCliBridge/UnityCliBridge.slnx - -# Local/generated histories -history.txt +# Generated test files for code index scalability testing +UnityCliBridge/Assets/Scripts/Generated/ +UnityCliBridge/Assets/Scripts/Generated.meta + +# Generated Unity scenes +UnityCliBridge/Assets/Scenes/Generated/ +UnityCliBridge/Assets/Scenes/Generated.meta + +UnityCliBridge/UnityCliBridge.slnx + +# Local/generated histories +history.txt + +# Generated by reference cache symbol indexer (Phase 2 build_or_update_index) +tests/fixtures/**/.unity-cli-index/