fix: v5.1 代码审查修复 — ESLint/CSV/测试隔离/CLI 去重

- ESLint: 迁移到 flat config ignores,删除已废弃的 .eslintignore
- CSV: Rust/TS 格式对齐,统一 type,path,enabled 3 列
- JSON: 导入导出统一为 {path, enabled} 对象格式
- scanner: 移除未使用的 max_threads 死代码 + TempDirGuard 测试清理
- profiles: rename_profile 添加目标存在检查
- CLI: 抽取 load_operate_save helper,简化 cmd_remove/cmd_edit
- PathTable: 抽取 usePathValidation hook,消除 set-state-in-effect
- 测试隔离: disabled/profiles 通过 #[cfg(test)] 重定向到 temp dir
- toolchain: 新增 rust-toolchain.toml 固定 stable-x86_64-pc-windows-gnu
- docs: 更新 CLAUDE.md/README.md 测试计数 + 架构树

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 17:31:04 +08:00
parent bce2dc8641
commit 21da3b2930
14 changed files with 430 additions and 214 deletions
+125
View File
@@ -0,0 +1,125 @@
import { useState, useEffect, useRef, useMemo } from 'react';
import { invoke } from '@tauri-apps/api/core';
import type { PathEntry } from '@/core/path-entry';
export type ValidationState = 'valid' | 'invalid' | 'unknown';
/**
* 异步验证路径目录是否真实存在 + 展开环境变量
* 缓存结果避免重复 IPC 调用。
* setState 仅在异步 .then() 回调中调用(符合 React 规则),
* 不存在路径的缓存清理通过 useMemo 派生。
*/
export function usePathValidation(paths: readonly PathEntry[]) {
const validatedRef = useRef<Set<string>>(new Set());
const expandedRef = useRef<Set<string>>(new Set());
const [validationCache, setValidationCache] = useState<Map<string, ValidationState>>(new Map());
const [expandedCache, setExpandedCache] = useState<Map<string, string>>(new Map());
// 仅保留当前 paths 中存在的条目(派生 state,不在 effect 中同步 setState
const currentKeys = useMemo(() => new Set(paths.map((p) => p.path)), [paths]);
const cleanedValidationCache = useMemo(() => {
const next = new Map(validationCache);
let changed = false;
for (const key of next.keys()) {
if (!currentKeys.has(key)) {
next.delete(key);
changed = true;
}
}
return changed ? next : validationCache;
}, [validationCache, currentKeys]);
const cleanedExpandedCache = useMemo(() => {
const next = new Map(expandedCache);
let changed = false;
for (const key of next.keys()) {
if (!currentKeys.has(key)) {
next.delete(key);
changed = true;
}
}
return changed ? next : expandedCache;
}, [expandedCache, currentKeys]);
// 同步清理 refref 不能在 render 期间修改,放在 effect 中不 setState 是安全的)
useEffect(() => {
for (const key of validatedRef.current) {
if (!currentKeys.has(key)) validatedRef.current.delete(key);
}
for (const key of expandedRef.current) {
if (!currentKeys.has(key)) expandedRef.current.delete(key);
}
}, [currentKeys]);
// 异步验证路径(setState 在 .then() 回调中,符合 React 规则)
useEffect(() => {
let cancelled = false;
const toValidate = paths.filter((p) => !validatedRef.current.has(p.path));
if (toValidate.length === 0) return;
const batch = toValidate.slice(0, 20);
Promise.all(
batch.map(
async (p): Promise<[string, ValidationState]> => {
try {
if (p.path.includes('%')) return [p.path, 'valid'];
const valid: boolean = await invoke('validate_path', { path: p.path });
return [p.path, valid ? 'valid' : 'invalid'];
} catch {
return [p.path, 'unknown'];
}
},
),
).then((results) => {
if (cancelled) return;
for (const [p] of results) validatedRef.current.add(p);
setValidationCache((prev) => {
const next = new Map(prev);
for (const [p, v] of results) next.set(p, v);
return next;
});
});
return () => {
cancelled = true;
};
}, [paths]);
// 异步展开环境变量(setState 在 .then() 回调中)
useEffect(() => {
let cancelled = false;
const toExpand = paths.filter(
(p) => p.path.includes('%') && !expandedRef.current.has(p.path),
);
if (toExpand.length === 0) return;
const batch = toExpand.slice(0, 20);
Promise.all(
batch.map(
async (p): Promise<[string, string]> => {
try {
const expanded: string = await invoke('expand_env_vars', { path: p.path });
return [p.path, expanded !== p.path ? expanded : ''];
} catch {
return [p.path, ''];
}
},
),
).then((results) => {
if (cancelled) return;
for (const [p] of results) expandedRef.current.add(p);
setExpandedCache((prev) => {
const next = new Map(prev);
for (const [p, v] of results) next.set(p, v);
return next;
});
});
return () => {
cancelled = true;
};
}, [paths]);
return { validationCache: cleanedValidationCache, expandedCache: cleanedExpandedCache };
}