diff --git a/README.md b/README.md index 6b1c15d..ef774a7 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,13 @@ This is a Claude Code skill designed to help developers conduct effective code r | File | Lines | Description | |------|-------|-------------| | **SKILL.md** | ~180 | Core principles + index (loads on skill activation) | -| **reference/react.md** | ~650 | React/Next.js patterns (on-demand) | -| **reference/vue.md** | ~200 | Vue 3 patterns (on-demand) | -| **reference/rust.md** | ~200 | Rust patterns (on-demand) | -| **reference/typescript.md** | ~100 | TypeScript/JS patterns (on-demand) | -| **reference/python.md** | ~60 | Python patterns (on-demand) | +| **reference/react.md** | ~870 | React 19/Next.js/TanStack Query v5 patterns (on-demand) | +| **reference/vue.md** | ~920 | Vue 3.5 patterns + Composition API (on-demand) | +| **reference/rust.md** | ~840 | Rust async/ownership/cancellation safety (on-demand) | +| **reference/typescript.md** | ~540 | TypeScript generics/strict mode/ESLint (on-demand) | +| **reference/python.md** | ~1070 | Python async/typing/pytest (on-demand) | -**Total: ~2,000+ lines** of review guidelines and code examples, loaded on-demand per language. +**Total: ~6,000+ lines** of review guidelines and code examples, loaded on-demand per language. ### Installation @@ -197,13 +197,13 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file | 文件 | 行数 | 描述 | |------|------|------| | **SKILL.md** | ~180 | 核心原则 + 索引(技能激活时加载)| -| **reference/react.md** | ~650 | React/Next.js 模式(按需加载)| -| **reference/vue.md** | ~200 | Vue 3 模式(按需加载)| -| **reference/rust.md** | ~200 | Rust 模式(按需加载)| -| **reference/typescript.md** | ~100 | TypeScript/JS 模式(按需加载)| -| **reference/python.md** | ~60 | Python 模式(按需加载)| +| **reference/react.md** | ~870 | React 19/Next.js/TanStack Query v5(按需加载)| +| **reference/vue.md** | ~920 | Vue 3.5 + Composition API(按需加载)| +| **reference/rust.md** | ~840 | Rust async/所有权/取消安全性(按需加载)| +| **reference/typescript.md** | ~540 | TypeScript 泛型/strict 模式/ESLint(按需加载)| +| **reference/python.md** | ~1070 | Python async/类型注解/pytest(按需加载)| -**总计:2,000+ 行**审查指南和代码示例,按语言按需加载。 +**总计:6,000+ 行**审查指南和代码示例,按语言按需加载。 ### 安装 diff --git a/reference/python.md b/reference/python.md index abcf22c..764db0e 100644 --- a/reference/python.md +++ b/reference/python.md @@ -1,6 +1,485 @@ # Python Code Review Guide -> Python 代码审查指南,覆盖常见陷阱和最佳实践。 +> Python 代码审查指南,覆盖类型注解、async/await、测试、异常处理、性能优化等核心主题。 + +## 目录 + +- [类型注解](#类型注解) +- [异步编程](#异步编程) +- [异常处理](#异常处理) +- [常见陷阱](#常见陷阱) +- [测试最佳实践](#测试最佳实践) +- [性能优化](#性能优化) +- [代码风格](#代码风格) +- [Review Checklist](#review-checklist) + +--- + +## 类型注解 + +### 基础类型注解 + +```python +# ❌ 没有类型注解,IDE 无法提供帮助 +def process_data(data, count): + return data[:count] + +# ✅ 使用类型注解 +def process_data(data: str, count: int) -> str: + return data[:count] + +# ✅ 复杂类型使用 typing 模块 +from typing import Optional, Union + +def find_user(user_id: int) -> Optional[User]: + """返回用户或 None""" + return db.get(user_id) + +def handle_input(value: Union[str, int]) -> str: + """接受字符串或整数""" + return str(value) +``` + +### 容器类型注解 + +```python +from typing import List, Dict, Set, Tuple, Sequence + +# ❌ 不精确的类型 +def get_names(users: list) -> list: + return [u.name for u in users] + +# ✅ 精确的容器类型(Python 3.9+ 可直接用 list[User]) +def get_names(users: List[User]) -> List[str]: + return [u.name for u in users] + +# ✅ 只读序列用 Sequence(更灵活) +def process_items(items: Sequence[str]) -> int: + return len(items) + +# ✅ 字典类型 +def count_words(text: str) -> Dict[str, int]: + words: Dict[str, int] = {} + for word in text.split(): + words[word] = words.get(word, 0) + 1 + return words + +# ✅ 元组(固定长度和类型) +def get_point() -> Tuple[float, float]: + return (1.0, 2.0) + +# ✅ 可变长度元组 +def get_scores() -> Tuple[int, ...]: + return (90, 85, 92, 88) +``` + +### 泛型与 TypeVar + +```python +from typing import TypeVar, Generic, List, Callable + +T = TypeVar('T') +K = TypeVar('K') +V = TypeVar('V') + +# ✅ 泛型函数 +def first(items: List[T]) -> T | None: + return items[0] if items else None + +# ✅ 有约束的 TypeVar +from typing import Hashable +H = TypeVar('H', bound=Hashable) + +def dedupe(items: List[H]) -> List[H]: + return list(set(items)) + +# ✅ 泛型类 +class Cache(Generic[K, V]): + def __init__(self) -> None: + self._data: Dict[K, V] = {} + + def get(self, key: K) -> V | None: + return self._data.get(key) + + def set(self, key: K, value: V) -> None: + self._data[key] = value +``` + +### Callable 与回调函数 + +```python +from typing import Callable, Awaitable + +# ✅ 函数类型注解 +Handler = Callable[[str, int], bool] + +def register_handler(name: str, handler: Handler) -> None: + handlers[name] = handler + +# ✅ 异步回调 +AsyncHandler = Callable[[str], Awaitable[dict]] + +async def fetch_with_handler( + url: str, + handler: AsyncHandler +) -> dict: + return await handler(url) + +# ✅ 返回函数的函数 +def create_multiplier(factor: int) -> Callable[[int], int]: + def multiplier(x: int) -> int: + return x * factor + return multiplier +``` + +### TypedDict 与结构化数据 + +```python +from typing import TypedDict, Required, NotRequired + +# ✅ 定义字典结构 +class UserDict(TypedDict): + id: int + name: str + email: str + age: NotRequired[int] # Python 3.11+ + +def create_user(data: UserDict) -> User: + return User(**data) + +# ✅ 部分必需字段 +class ConfigDict(TypedDict, total=False): + debug: bool + timeout: int + host: Required[str] # 这个必须有 +``` + +### Protocol 与结构化子类型 + +```python +from typing import Protocol, runtime_checkable + +# ✅ 定义协议(鸭子类型的类型检查) +class Readable(Protocol): + def read(self, size: int = -1) -> bytes: ... + +class Closeable(Protocol): + def close(self) -> None: ... + +# 组合协议 +class ReadableCloseable(Readable, Closeable, Protocol): + pass + +def process_stream(stream: Readable) -> bytes: + return stream.read() + +# ✅ 运行时可检查的协议 +@runtime_checkable +class Drawable(Protocol): + def draw(self) -> None: ... + +def render(obj: object) -> None: + if isinstance(obj, Drawable): # 运行时检查 + obj.draw() +``` + +--- + +## 异步编程 + +### async/await 基础 + +```python +import asyncio + +# ❌ 同步阻塞调用 +def fetch_all_sync(urls: list[str]) -> list[str]: + results = [] + for url in urls: + results.append(requests.get(url).text) # 串行执行 + return results + +# ✅ 异步并发调用 +async def fetch_url(url: str) -> str: + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + return await response.text() + +async def fetch_all(urls: list[str]) -> list[str]: + tasks = [fetch_url(url) for url in urls] + return await asyncio.gather(*tasks) # 并发执行 +``` + +### 异步上下文管理器 + +```python +from contextlib import asynccontextmanager +from typing import AsyncIterator + +# ✅ 异步上下文管理器类 +class AsyncDatabase: + async def __aenter__(self) -> 'AsyncDatabase': + await self.connect() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + await self.disconnect() + +# ✅ 使用装饰器 +@asynccontextmanager +async def get_connection() -> AsyncIterator[Connection]: + conn = await create_connection() + try: + yield conn + finally: + await conn.close() + +async def query_data(): + async with get_connection() as conn: + return await conn.fetch("SELECT * FROM users") +``` + +### 异步迭代器 + +```python +from typing import AsyncIterator + +# ✅ 异步生成器 +async def fetch_pages(url: str) -> AsyncIterator[dict]: + page = 1 + while True: + data = await fetch_page(url, page) + if not data['items']: + break + yield data + page += 1 + +# ✅ 使用异步迭代 +async def process_all_pages(): + async for page in fetch_pages("https://api.example.com"): + await process_page(page) +``` + +### 任务管理与取消 + +```python +import asyncio + +# ❌ 忘记处理取消 +async def bad_worker(): + while True: + await do_work() # 无法正常取消 + +# ✅ 正确处理取消 +async def good_worker(): + try: + while True: + await do_work() + except asyncio.CancelledError: + await cleanup() # 清理资源 + raise # 重新抛出,让调用者知道已取消 + +# ✅ 超时控制 +async def fetch_with_timeout(url: str) -> str: + try: + async with asyncio.timeout(10): # Python 3.11+ + return await fetch_url(url) + except asyncio.TimeoutError: + return "" + +# ✅ 任务组(Python 3.11+) +async def fetch_multiple(): + async with asyncio.TaskGroup() as tg: + task1 = tg.create_task(fetch_url("url1")) + task2 = tg.create_task(fetch_url("url2")) + # 所有任务完成后自动等待,异常会传播 + return task1.result(), task2.result() +``` + +### 同步与异步混合 + +```python +import asyncio +from concurrent.futures import ThreadPoolExecutor + +# ✅ 在异步代码中运行同步函数 +async def run_sync_in_async(): + loop = asyncio.get_event_loop() + # 使用线程池执行阻塞操作 + result = await loop.run_in_executor( + None, # 默认线程池 + blocking_io_function, + arg1, arg2 + ) + return result + +# ✅ 在同步代码中运行异步函数 +def run_async_in_sync(): + return asyncio.run(async_function()) + +# ❌ 不要在异步代码中使用 time.sleep +async def bad_delay(): + time.sleep(1) # 会阻塞整个事件循环! + +# ✅ 使用 asyncio.sleep +async def good_delay(): + await asyncio.sleep(1) +``` + +### 信号量与限流 + +```python +import asyncio + +# ✅ 使用信号量限制并发 +async def fetch_with_limit(urls: list[str], max_concurrent: int = 10): + semaphore = asyncio.Semaphore(max_concurrent) + + async def fetch_one(url: str) -> str: + async with semaphore: + return await fetch_url(url) + + return await asyncio.gather(*[fetch_one(url) for url in urls]) + +# ✅ 使用 asyncio.Queue 实现生产者-消费者 +async def producer_consumer(): + queue: asyncio.Queue[str] = asyncio.Queue(maxsize=100) + + async def producer(): + for item in items: + await queue.put(item) + await queue.put(None) # 结束信号 + + async def consumer(): + while True: + item = await queue.get() + if item is None: + break + await process(item) + queue.task_done() + + await asyncio.gather(producer(), consumer()) +``` + +--- + +## 异常处理 + +### 异常捕获最佳实践 + +```python +# ❌ Catching too broad +try: + result = risky_operation() +except: # Catches everything, even KeyboardInterrupt! + pass + +# ❌ 捕获 Exception 但不处理 +try: + result = risky_operation() +except Exception: + pass # 吞掉所有异常,难以调试 + +# ✅ Catch specific exceptions +try: + result = risky_operation() +except ValueError as e: + logger.error(f"Invalid value: {e}") + raise +except IOError as e: + logger.error(f"IO error: {e}") + return default_value + +# ✅ 多个异常类型 +try: + result = parse_and_process(data) +except (ValueError, TypeError, KeyError) as e: + logger.error(f"Data error: {e}") + raise DataProcessingError(str(e)) from e +``` + +### 异常链 + +```python +# ❌ 丢失原始异常信息 +try: + result = external_api.call() +except APIError as e: + raise RuntimeError("API failed") # 丢失了原因 + +# ✅ 使用 from 保留异常链 +try: + result = external_api.call() +except APIError as e: + raise RuntimeError("API failed") from e + +# ✅ 显式断开异常链(少见情况) +try: + result = external_api.call() +except APIError: + raise RuntimeError("API failed") from None +``` + +### 自定义异常 + +```python +# ✅ 定义业务异常层次结构 +class AppError(Exception): + """应用基础异常""" + pass + +class ValidationError(AppError): + """数据验证错误""" + def __init__(self, field: str, message: str): + self.field = field + self.message = message + super().__init__(f"{field}: {message}") + +class NotFoundError(AppError): + """资源未找到""" + def __init__(self, resource: str, id: str | int): + self.resource = resource + self.id = id + super().__init__(f"{resource} with id {id} not found") + +# 使用 +def get_user(user_id: int) -> User: + user = db.get(user_id) + if not user: + raise NotFoundError("User", user_id) + return user +``` + +### 上下文管理器中的异常 + +```python +from contextlib import contextmanager + +# ✅ 正确处理上下文管理器中的异常 +@contextmanager +def transaction(): + conn = get_connection() + try: + yield conn + conn.commit() + except Exception: + conn.rollback() + raise + finally: + conn.close() + +# ✅ 使用 ExceptionGroup(Python 3.11+) +def process_batch(items: list) -> None: + errors = [] + for item in items: + try: + process(item) + except Exception as e: + errors.append(e) + + if errors: + raise ExceptionGroup("Batch processing failed", errors) +``` --- @@ -14,29 +493,23 @@ def add_item(item, items=[]): # Bug! Shared across calls items.append(item) return items +# 问题演示 +add_item(1) # [1] +add_item(2) # [1, 2] 而不是 [2]! + # ✅ Use None as default def add_item(item, items=None): if items is None: items = [] items.append(item) return items -``` -### 异常捕获过宽 +# ✅ 或使用 dataclass 的 field +from dataclasses import dataclass, field -```python -# ❌ Catching too broad -try: - result = risky_operation() -except: # Catches everything, even KeyboardInterrupt! - pass - -# ✅ Catch specific exceptions -try: - result = risky_operation() -except ValueError as e: - logger.error(f"Invalid value: {e}") - raise +@dataclass +class Container: + items: list = field(default_factory=list) ``` ### 可变类属性 @@ -46,32 +519,551 @@ except ValueError as e: class User: permissions = [] # Shared across all instances! +# 问题演示 +u1 = User() +u2 = User() +u1.permissions.append("admin") +print(u2.permissions) # ["admin"] - 被意外共享! + # ✅ Initialize in __init__ class User: def __init__(self): self.permissions = [] + +# ✅ 使用 dataclass +@dataclass +class User: + permissions: list = field(default_factory=list) +``` + +### 循环中的闭包 + +```python +# ❌ 闭包捕获循环变量 +funcs = [] +for i in range(3): + funcs.append(lambda: i) + +print([f() for f in funcs]) # [2, 2, 2] 而不是 [0, 1, 2]! + +# ✅ 使用默认参数捕获值 +funcs = [] +for i in range(3): + funcs.append(lambda i=i: i) + +print([f() for f in funcs]) # [0, 1, 2] + +# ✅ 使用 functools.partial +from functools import partial + +funcs = [partial(lambda x: x, i) for i in range(3)] +``` + +### is vs == + +```python +# ❌ 用 is 比较值 +if x is 1000: # 可能不工作! + pass + +# Python 会缓存小整数 (-5 到 256) +a = 256 +b = 256 +a is b # True + +a = 257 +b = 257 +a is b # False! + +# ✅ 用 == 比较值 +if x == 1000: + pass + +# ✅ is 只用于 None 和单例 +if x is None: + pass + +if x is True: # 严格检查布尔值 + pass +``` + +### 字符串拼接性能 + +```python +# ❌ 循环中拼接字符串 +result = "" +for item in large_list: + result += str(item) # O(n²) 复杂度 + +# ✅ 使用 join +result = "".join(str(item) for item in large_list) # O(n) + +# ✅ 使用 StringIO 构建大字符串 +from io import StringIO + +buffer = StringIO() +for item in large_list: + buffer.write(str(item)) +result = buffer.getvalue() ``` --- -## Python Review Checklist +## 测试最佳实践 + +### pytest 基础 + +```python +import pytest + +# ✅ 清晰的测试命名 +def test_user_creation_with_valid_email(): + user = User(email="test@example.com") + assert user.email == "test@example.com" + +def test_user_creation_with_invalid_email_raises_error(): + with pytest.raises(ValidationError): + User(email="invalid") + +# ✅ 使用参数化测试 +@pytest.mark.parametrize("input,expected", [ + ("hello", "HELLO"), + ("World", "WORLD"), + ("", ""), + ("123", "123"), +]) +def test_uppercase(input: str, expected: str): + assert input.upper() == expected + +# ✅ 测试异常 +def test_division_by_zero(): + with pytest.raises(ZeroDivisionError) as exc_info: + 1 / 0 + assert "division by zero" in str(exc_info.value) +``` + +### Fixtures + +```python +import pytest +from typing import Generator + +# ✅ 基础 fixture +@pytest.fixture +def user() -> User: + return User(name="Test User", email="test@example.com") + +def test_user_name(user: User): + assert user.name == "Test User" + +# ✅ 带清理的 fixture +@pytest.fixture +def database() -> Generator[Database, None, None]: + db = Database() + db.connect() + yield db + db.disconnect() # 测试后清理 + +# ✅ 异步 fixture +@pytest.fixture +async def async_client() -> AsyncGenerator[AsyncClient, None]: + async with AsyncClient() as client: + yield client + +# ✅ 共享 fixture(conftest.py) +# conftest.py +@pytest.fixture(scope="session") +def app(): + """整个测试会话共享的 app 实例""" + return create_app() + +@pytest.fixture(scope="module") +def db(app): + """每个测试模块共享的数据库连接""" + return app.db +``` + +### Mock 与 Patch + +```python +from unittest.mock import Mock, patch, AsyncMock + +# ✅ Mock 外部依赖 +def test_send_email(): + mock_client = Mock() + mock_client.send.return_value = True + + service = EmailService(client=mock_client) + result = service.send_welcome_email("user@example.com") + + assert result is True + mock_client.send.assert_called_once_with( + to="user@example.com", + subject="Welcome!", + body=ANY, + ) + +# ✅ Patch 模块级函数 +@patch("myapp.services.external_api.call") +def test_with_patched_api(mock_call): + mock_call.return_value = {"status": "ok"} + + result = process_data() + + assert result["status"] == "ok" + +# ✅ 异步 Mock +async def test_async_function(): + mock_fetch = AsyncMock(return_value={"data": "test"}) + + with patch("myapp.client.fetch", mock_fetch): + result = await get_data() + + assert result == {"data": "test"} +``` + +### 测试组织 + +```python +# ✅ 使用类组织相关测试 +class TestUserAuthentication: + """用户认证相关测试""" + + def test_login_with_valid_credentials(self, user): + assert authenticate(user.email, "password") is True + + def test_login_with_invalid_password(self, user): + assert authenticate(user.email, "wrong") is False + + def test_login_locks_after_failed_attempts(self, user): + for _ in range(5): + authenticate(user.email, "wrong") + assert user.is_locked is True + +# ✅ 使用 mark 标记测试 +@pytest.mark.slow +def test_large_data_processing(): + pass + +@pytest.mark.integration +def test_database_connection(): + pass + +# 运行特定标记的测试:pytest -m "not slow" +``` + +### 覆盖率与质量 + +```python +# pytest.ini 或 pyproject.toml +[tool.pytest.ini_options] +addopts = "--cov=myapp --cov-report=term-missing --cov-fail-under=80" +testpaths = ["tests"] + +# ✅ 测试边界情况 +def test_empty_input(): + assert process([]) == [] + +def test_none_input(): + with pytest.raises(TypeError): + process(None) + +def test_large_input(): + large_data = list(range(100000)) + result = process(large_data) + assert len(result) == 100000 +``` + +--- + +## 性能优化 + +### 数据结构选择 + +```python +# ❌ 列表查找 O(n) +if item in large_list: # 慢 + pass + +# ✅ 集合查找 O(1) +large_set = set(large_list) +if item in large_set: # 快 + pass + +# ✅ 使用 collections 模块 +from collections import Counter, defaultdict, deque + +# 计数 +word_counts = Counter(words) +most_common = word_counts.most_common(10) + +# 默认字典 +graph = defaultdict(list) +graph[node].append(neighbor) + +# 双端队列(两端操作 O(1)) +queue = deque() +queue.appendleft(item) # O(1) vs list.insert(0, item) O(n) +``` + +### 生成器与迭代器 + +```python +# ❌ 一次性加载所有数据 +def get_all_users(): + return [User(row) for row in db.fetch_all()] # 内存占用大 + +# ✅ 使用生成器 +def get_all_users(): + for row in db.fetch_all(): + yield User(row) # 懒加载 + +# ✅ 生成器表达式 +sum_of_squares = sum(x**2 for x in range(1000000)) # 不创建列表 + +# ✅ itertools 模块 +from itertools import islice, chain, groupby + +# 只取前 10 个 +first_10 = list(islice(infinite_generator(), 10)) + +# 链接多个迭代器 +all_items = chain(list1, list2, list3) + +# 分组 +for key, group in groupby(sorted(items, key=get_key), key=get_key): + process_group(key, list(group)) +``` + +### 缓存 + +```python +from functools import lru_cache, cache + +# ✅ LRU 缓存 +@lru_cache(maxsize=128) +def expensive_computation(n: int) -> int: + return sum(i**2 for i in range(n)) + +# ✅ 无限缓存(Python 3.9+) +@cache +def fibonacci(n: int) -> int: + if n < 2: + return n + return fibonacci(n - 1) + fibonacci(n - 2) + +# ✅ 手动缓存(需要更多控制时) +class DataService: + def __init__(self): + self._cache: dict[str, Any] = {} + self._cache_ttl: dict[str, float] = {} + + def get_data(self, key: str) -> Any: + if key in self._cache: + if time.time() < self._cache_ttl[key]: + return self._cache[key] + + data = self._fetch_data(key) + self._cache[key] = data + self._cache_ttl[key] = time.time() + 300 # 5 分钟 + return data +``` + +### 并行处理 + +```python +from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor + +# ✅ IO 密集型使用线程池 +def fetch_all_urls(urls: list[str]) -> list[str]: + with ThreadPoolExecutor(max_workers=10) as executor: + results = list(executor.map(fetch_url, urls)) + return results + +# ✅ CPU 密集型使用进程池 +def process_large_dataset(data: list) -> list: + with ProcessPoolExecutor() as executor: + results = list(executor.map(heavy_computation, data)) + return results + +# ✅ 使用 as_completed 获取最先完成的结果 +from concurrent.futures import as_completed + +with ThreadPoolExecutor() as executor: + futures = {executor.submit(fetch, url): url for url in urls} + for future in as_completed(futures): + url = futures[future] + try: + result = future.result() + except Exception as e: + print(f"{url} failed: {e}") +``` + +--- + +## 代码风格 + +### PEP 8 要点 + +```python +# ✅ 命名规范 +class MyClass: # 类名 PascalCase + MAX_SIZE = 100 # 常量 UPPER_SNAKE_CASE + + def method_name(self): # 方法 snake_case + local_var = 1 # 变量 snake_case + +# ✅ 导入顺序 +# 1. 标准库 +import os +import sys +from typing import Optional + +# 2. 第三方库 +import numpy as np +import pandas as pd + +# 3. 本地模块 +from myapp import config +from myapp.utils import helper + +# ✅ 行长度限制(79 或 88 字符) +# 长表达式的换行 +result = ( + long_function_name(arg1, arg2, arg3) + + another_long_function(arg4, arg5) +) + +# ✅ 空行规范 +class MyClass: + """类文档字符串""" + + def method_one(self): + pass + + def method_two(self): # 方法间一个空行 + pass + + +def top_level_function(): # 顶层定义间两个空行 + pass +``` + +### 文档字符串 + +```python +# ✅ Google 风格文档字符串 +def calculate_area(width: float, height: float) -> float: + """计算矩形面积。 + + Args: + width: 矩形的宽度(必须为正数)。 + height: 矩形的高度(必须为正数)。 + + Returns: + 矩形的面积。 + + Raises: + ValueError: 如果 width 或 height 为负数。 + + Example: + >>> calculate_area(3, 4) + 12.0 + """ + if width < 0 or height < 0: + raise ValueError("Dimensions must be positive") + return width * height + +# ✅ 类文档字符串 +class DataProcessor: + """处理和转换数据的工具类。 + + Attributes: + source: 数据来源路径。 + format: 输出格式('json' 或 'csv')。 + + Example: + >>> processor = DataProcessor("data.csv") + >>> processor.process() + """ +``` + +### 现代 Python 特性 + +```python +# ✅ f-string(Python 3.6+) +name = "World" +print(f"Hello, {name}!") + +# 带表达式 +print(f"Result: {1 + 2 = }") # "Result: 1 + 2 = 3" + +# ✅ 海象运算符(Python 3.8+) +if (n := len(items)) > 10: + print(f"List has {n} items") + +# ✅ 位置参数分隔符(Python 3.8+) +def greet(name, /, greeting="Hello", *, punctuation="!"): + """name 只能位置传参,punctuation 只能关键字传参""" + return f"{greeting}, {name}{punctuation}" + +# ✅ 模式匹配(Python 3.10+) +def handle_response(response: dict): + match response: + case {"status": "ok", "data": data}: + return process_data(data) + case {"status": "error", "message": msg}: + raise APIError(msg) + case _: + raise ValueError("Unknown response format") +``` + +--- + +## Review Checklist + +### 类型安全 +- [ ] 函数有类型注解(参数和返回值) +- [ ] 使用 `Optional` 明确可能为 None +- [ ] 泛型类型正确使用 +- [ ] mypy 检查通过(无错误) +- [ ] 避免使用 `Any`,必要时添加注释说明 + +### 异步代码 +- [ ] async/await 正确配对使用 +- [ ] 没有在异步代码中使用阻塞调用 +- [ ] 正确处理 `CancelledError` +- [ ] 使用 `asyncio.gather` 或 `TaskGroup` 并发执行 +- [ ] 资源正确清理(async context manager) + +### 异常处理 +- [ ] 捕获特定异常类型,不使用裸 `except:` +- [ ] 异常链使用 `from` 保留原因 +- [ ] 自定义异常继承自合适的基类 +- [ ] 异常信息有意义,便于调试 ### 数据结构 - [ ] 没有使用可变默认参数(list、dict、set) - [ ] 类属性不是可变对象 -- [ ] 理解浅拷贝和深拷贝的区别 - -### 异常处理 -- [ ] 捕获特定异常类型,不使用裸 `except:` -- [ ] 异常信息有意义,便于调试 -- [ ] 必要时重新抛出异常(`raise`) - -### 性能 +- [ ] 选择正确的数据结构(set vs list 查找) - [ ] 大数据集使用生成器而非列表 -- [ ] 避免循环中重复创建对象 -- [ ] 使用 `collections` 模块的高效数据结构 + +### 测试 +- [ ] 测试覆盖率达标(建议 ≥80%) +- [ ] 测试命名清晰描述测试场景 +- [ ] 边界情况有测试覆盖 +- [ ] Mock 正确隔离外部依赖 +- [ ] 异步代码有对应的异步测试 ### 代码风格 - [ ] 遵循 PEP 8 风格指南 -- [ ] 使用类型注解(type hints) - [ ] 函数和类有 docstring +- [ ] 导入顺序正确(标准库、第三方、本地) +- [ ] 命名一致且有意义 +- [ ] 使用现代 Python 特性(f-string、walrus operator 等) + +### 性能 +- [ ] 避免循环中重复创建对象 +- [ ] 字符串拼接使用 join +- [ ] 合理使用缓存(@lru_cache) +- [ ] IO/CPU 密集型使用合适的并行方式 diff --git a/reference/react.md b/reference/react.md index f41bf31..75a7b19 100644 --- a/reference/react.md +++ b/reference/react.md @@ -630,6 +630,20 @@ function GoodQuery({ filters }) { ### useSuspenseQuery +> **重要限制**:useSuspenseQuery 与 useQuery 有显著差异,选择前需了解其限制。 + +#### useSuspenseQuery 的限制 + +| 特性 | useQuery | useSuspenseQuery | +|------|----------|------------------| +| `enabled` 选项 | ✅ 支持 | ❌ 不支持 | +| `placeholderData` | ✅ 支持 | ❌ 不支持 | +| `data` 类型 | `T \| undefined` | `T`(保证有值)| +| 错误处理 | `error` 属性 | 抛出到 Error Boundary | +| 加载状态 | `isLoading` 属性 | 挂起到 Suspense | + +#### 不支持 enabled 的替代方案 + ```tsx // ❌ 使用 useQuery + enabled 实现条件查询 function BadSuspenseQuery({ userId }) { @@ -660,6 +674,64 @@ function Parent({ userId }) { } ``` +#### 错误处理差异 + +```tsx +// ❌ useSuspenseQuery 没有 error 属性 +function BadErrorHandling() { + const { data, error } = useSuspenseQuery({...}); + if (error) return ; // error 总是 null! +} + +// ✅ 使用 Error Boundary 处理错误 +function GoodErrorHandling() { + return ( + }> + }> + + + + ); +} + +function DataComponent() { + // 错误会抛出到 Error Boundary + const { data } = useSuspenseQuery({ + queryKey: ['data'], + queryFn: fetchData, + }); + return ; +} +``` + +#### 何时选择 useSuspenseQuery + +```tsx +// ✅ 适合场景: +// 1. 数据总是需要的(无条件查询) +// 2. 组件必须有数据才能渲染 +// 3. 使用 React 19 的 Suspense 模式 +// 4. 服务端组件 + 客户端 hydration + +// ❌ 不适合场景: +// 1. 条件查询(根据用户操作触发) +// 2. 需要 placeholderData 或初始数据 +// 3. 需要在组件内处理 loading/error 状态 +// 4. 多个查询有依赖关系 + +// ✅ 多个独立查询用 useSuspenseQueries +function MultipleQueries({ userId }) { + const [userQuery, postsQuery] = useSuspenseQueries({ + queries: [ + { queryKey: ['user', userId], queryFn: () => fetchUser(userId) }, + { queryKey: ['posts', userId], queryFn: () => fetchPosts(userId) }, + ], + }); + // 两个查询并行执行,都完成后组件渲染 + return ; +} +``` + ### 乐观更新 (v5 简化) ```tsx diff --git a/reference/rust.md b/reference/rust.md index a66f818..1fa062c 100644 --- a/reference/rust.md +++ b/reference/rust.md @@ -1,6 +1,18 @@ # Rust Code Review Guide -> Rust 代码审查指南。编译器能捕获内存安全问题,但审查者需要关注编译器无法检测的问题——业务逻辑、API 设计、性能和可维护性。 +> Rust 代码审查指南。编译器能捕获内存安全问题,但审查者需要关注编译器无法检测的问题——业务逻辑、API 设计、性能、取消安全性和可维护性。 + +## 目录 + +- [所有权与借用](#所有权与借用) +- [Unsafe 代码审查](#unsafe-代码审查最关键) +- [异步代码](#异步代码) +- [取消安全性](#取消安全性) +- [spawn vs await](#spawn-vs-await) +- [错误处理](#错误处理) +- [性能](#性能) +- [Trait 设计](#trait-设计) +- [Review Checklist](#rust-review-checklist) --- @@ -19,6 +31,16 @@ fn bad_process(data: &Data) -> Result<()> { fn good_process(data: &Data) -> Result<()> { expensive_operation(data) // 传递引用 } + +// ✅ 如果确实需要 clone,添加注释说明原因 +fn justified_clone(data: &Data) -> Result<()> { + // Clone needed: data will be moved to spawned task + let owned = data.clone(); + tokio::spawn(async move { + process(owned).await + }); + Ok(()) +} ``` ### Arc> 的使用 @@ -33,12 +55,54 @@ struct BadService { struct GoodService { cache: HashMap, // 单一所有者 } + +// ✅ 如果确实需要并发访问,考虑更好的数据结构 +use dashmap::DashMap; + +struct ConcurrentService { + cache: DashMap, // 更细粒度的锁 +} +``` + +### Cow (Copy-on-Write) 模式 + +```rust +use std::borrow::Cow; + +// ❌ 总是分配新字符串 +fn bad_process_name(name: &str) -> String { + if name.is_empty() { + "Unknown".to_string() // 分配 + } else { + name.to_string() // 不必要的分配 + } +} + +// ✅ 使用 Cow 避免不必要的分配 +fn good_process_name(name: &str) -> Cow<'_, str> { + if name.is_empty() { + Cow::Borrowed("Unknown") // 静态字符串,无分配 + } else { + Cow::Borrowed(name) // 借用原始数据 + } +} + +// ✅ 只在需要修改时才分配 +fn normalize_name(name: &str) -> Cow<'_, str> { + if name.chars().any(|c| c.is_uppercase()) { + Cow::Owned(name.to_lowercase()) // 需要修改,分配 + } else { + Cow::Borrowed(name) // 无需修改,借用 + } +} ``` --- ## Unsafe 代码审查(最关键!) +### 基本要求 + ```rust // ❌ unsafe 没有安全文档——这是红旗 unsafe fn bad_transmute(t: T) -> U { @@ -59,6 +123,69 @@ unsafe fn documented_transmute(t: T) -> U { } ``` +### Unsafe 块注释 + +```rust +// ❌ 没有解释的 unsafe 块 +fn bad_get_unchecked(slice: &[u8], index: usize) -> u8 { + unsafe { *slice.get_unchecked(index) } +} + +// ✅ 每个 unsafe 块必须有 SAFETY 注释 +fn good_get_unchecked(slice: &[u8], index: usize) -> u8 { + debug_assert!(index < slice.len(), "index out of bounds"); + // SAFETY: We verified index < slice.len() via debug_assert. + // In release builds, callers must ensure valid index. + unsafe { *slice.get_unchecked(index) } +} + +// ✅ 封装 unsafe 提供安全 API +pub fn checked_get(slice: &[u8], index: usize) -> Option { + if index < slice.len() { + // SAFETY: bounds check performed above + Some(unsafe { *slice.get_unchecked(index) }) + } else { + None + } +} +``` + +### 常见 unsafe 模式 + +```rust +// ✅ FFI 边界 +extern "C" { + fn external_function(ptr: *const u8, len: usize) -> i32; +} + +pub fn safe_wrapper(data: &[u8]) -> Result { + // SAFETY: data.as_ptr() is valid for data.len() bytes, + // and external_function only reads from the buffer. + let result = unsafe { + external_function(data.as_ptr(), data.len()) + }; + if result < 0 { + Err(Error::from_code(result)) + } else { + Ok(result) + } +} + +// ✅ 性能关键路径的 unsafe +pub fn fast_copy(src: &[u8], dst: &mut [u8]) { + assert_eq!(src.len(), dst.len(), "slices must be equal length"); + // SAFETY: src and dst are valid slices of equal length, + // and dst is mutable so no aliasing. + unsafe { + std::ptr::copy_nonoverlapping( + src.as_ptr(), + dst.as_mut_ptr(), + src.len() + ); + } +} +``` + --- ## 异步代码 @@ -73,9 +200,19 @@ async fn bad_async() { } // ✅ 使用异步 API -async fn good_async() { +async fn good_async() -> Result { let data = tokio::fs::read_to_string("file.txt").await?; tokio::time::sleep(Duration::from_secs(1)).await; + Ok(data) +} + +// ✅ 如果必须使用阻塞操作,用 spawn_blocking +async fn with_blocking() -> Result { + let result = tokio::task::spawn_blocking(|| { + // 这里可以安全地进行阻塞操作 + expensive_cpu_computation() + }).await?; + Ok(result) } ``` @@ -89,12 +226,323 @@ async fn bad_lock(mutex: &std::sync::Mutex) { process(&guard); } -// ✅ 最小化锁范围,或使用 tokio::sync::Mutex -async fn good_lock(mutex: &std::sync::Mutex) { - let data = mutex.lock().unwrap().clone(); // 立即释放 +// ✅ 方案1:最小化锁范围 +async fn good_lock_scoped(mutex: &std::sync::Mutex) { + let data = { + let guard = mutex.lock().unwrap(); + guard.clone() // 立即释放锁 + }; async_operation().await; process(&data); } + +// ✅ 方案2:使用 tokio::sync::Mutex(可跨 await) +async fn good_lock_tokio(mutex: &tokio::sync::Mutex) { + let guard = mutex.lock().await; + async_operation().await; // OK: tokio Mutex 设计为可跨 await + process(&guard); +} + +// 💡 选择指南: +// - std::sync::Mutex:低竞争、短临界区、不跨 await +// - tokio::sync::Mutex:需要跨 await、高竞争场景 +``` + +### 异步 trait 方法 + +```rust +// ❌ async trait 方法的陷阱(旧版本) +#[async_trait] +trait BadRepository { + async fn find(&self, id: i64) -> Option; // 隐式 Box +} + +// ✅ Rust 1.75+:原生 async trait 方法 +trait Repository { + async fn find(&self, id: i64) -> Option; + + // 返回具体 Future 类型以避免 allocation + fn find_many(&self, ids: &[i64]) -> impl Future> + Send; +} + +// ✅ 对于需要 dyn 的场景 +trait DynRepository: Send + Sync { + fn find(&self, id: i64) -> Pin> + Send + '_>>; +} +``` + +--- + +## 取消安全性 + +### 什么是取消安全 + +```rust +// 当一个 Future 在 .await 点被 drop 时,它处于什么状态? +// 取消安全的 Future:可以在任何 await 点安全取消 +// 取消不安全的 Future:取消可能导致数据丢失或不一致状态 + +// ❌ 取消不安全的例子 +async fn cancel_unsafe(conn: &mut Connection) -> Result<()> { + let data = receive_data().await; // 如果这里被取消... + conn.send_ack().await; // ...确认永远不会发送,数据可能丢失 + Ok(()) +} + +// ✅ 取消安全的版本 +async fn cancel_safe(conn: &mut Connection) -> Result<()> { + // 使用事务或原子操作确保一致性 + let transaction = conn.begin_transaction().await?; + let data = receive_data().await; + transaction.commit_with_ack(data).await?; // 原子操作 + Ok(()) +} +``` + +### select! 中的取消安全 + +```rust +use tokio::select; + +// ❌ 在 select! 中使用取消不安全的 Future +async fn bad_select(stream: &mut TcpStream) { + let mut buffer = vec![0u8; 1024]; + loop { + select! { + // 如果 timeout 先完成,read 被取消 + // 部分读取的数据可能丢失! + result = stream.read(&mut buffer) => { + handle_data(&buffer[..result?]); + } + _ = tokio::time::sleep(Duration::from_secs(5)) => { + println!("Timeout"); + } + } + } +} + +// ✅ 使用取消安全的 API +async fn good_select(stream: &mut TcpStream) { + let mut buffer = vec![0u8; 1024]; + loop { + select! { + // tokio::io::AsyncReadExt::read 是取消安全的 + // 取消时,未读取的数据留在流中 + result = stream.read(&mut buffer) => { + match result { + Ok(0) => break, // EOF + Ok(n) => handle_data(&buffer[..n]), + Err(e) => return Err(e), + } + } + _ = tokio::time::sleep(Duration::from_secs(5)) => { + println!("Timeout, retrying..."); + } + } + } +} + +// ✅ 使用 tokio::pin! 确保 Future 可以安全重用 +async fn pinned_select() { + let sleep = tokio::time::sleep(Duration::from_secs(10)); + tokio::pin!(sleep); + + loop { + select! { + _ = &mut sleep => { + println!("Timer elapsed"); + break; + } + data = receive_data() => { + process(data).await; + // sleep 继续倒计时,不会重置 + } + } + } +} +``` + +### 文档化取消安全性 + +```rust +/// Reads a complete message from the stream. +/// +/// # Cancel Safety +/// +/// This method is **not** cancel safe. If cancelled while reading, +/// partial data may be lost and the stream state becomes undefined. +/// Use `read_message_cancel_safe` if cancellation is expected. +async fn read_message(stream: &mut TcpStream) -> Result { + let len = stream.read_u32().await?; + let mut buffer = vec![0u8; len as usize]; + stream.read_exact(&mut buffer).await?; + Ok(Message::from_bytes(&buffer)) +} + +/// Reads a message with cancel safety. +/// +/// # Cancel Safety +/// +/// This method is cancel safe. If cancelled, any partial data +/// is preserved in the internal buffer for the next call. +async fn read_message_cancel_safe(reader: &mut BufferedReader) -> Result { + reader.read_message_buffered().await +} +``` + +--- + +## spawn vs await + +### 何时使用 spawn + +```rust +// ❌ 不必要的 spawn——增加开销,失去结构化并发 +async fn bad_unnecessary_spawn() { + let handle = tokio::spawn(async { + simple_operation().await + }); + handle.await.unwrap(); // 为什么不直接 await? +} + +// ✅ 直接 await 简单操作 +async fn good_direct_await() { + simple_operation().await; +} + +// ✅ spawn 用于真正的并行执行 +async fn good_parallel_spawn() { + let task1 = tokio::spawn(fetch_from_service_a()); + let task2 = tokio::spawn(fetch_from_service_b()); + + // 两个请求并行执行 + let (result1, result2) = tokio::try_join!(task1, task2)?; +} + +// ✅ spawn 用于后台任务(fire-and-forget) +async fn good_background_spawn() { + // 启动后台任务,不等待完成 + tokio::spawn(async { + cleanup_old_sessions().await; + log_metrics().await; + }); + + // 继续执行其他工作 + handle_request().await; +} +``` + +### spawn 的 'static 要求 + +```rust +// ❌ spawn 的 Future 必须是 'static +async fn bad_spawn_borrow(data: &Data) { + tokio::spawn(async { + process(data).await; // Error: `data` 不是 'static + }); +} + +// ✅ 方案1:克隆数据 +async fn good_spawn_clone(data: &Data) { + let owned = data.clone(); + tokio::spawn(async move { + process(&owned).await; + }); +} + +// ✅ 方案2:使用 Arc 共享 +async fn good_spawn_arc(data: Arc) { + let data = Arc::clone(&data); + tokio::spawn(async move { + process(&data).await; + }); +} + +// ✅ 方案3:使用作用域任务(tokio-scoped 或 async-scoped) +async fn good_scoped_spawn(data: &Data) { + // 假设使用 async-scoped crate + async_scoped::scope(|s| async { + s.spawn(async { + process(data).await; // 可以借用 + }); + }).await; +} +``` + +### JoinHandle 错误处理 + +```rust +// ❌ 忽略 spawn 的错误 +async fn bad_ignore_spawn_error() { + let handle = tokio::spawn(async { + risky_operation().await + }); + let _ = handle.await; // 忽略了 panic 和错误 +} + +// ✅ 正确处理 JoinHandle 结果 +async fn good_handle_spawn_error() -> Result<()> { + let handle = tokio::spawn(async { + risky_operation().await + }); + + match handle.await { + Ok(Ok(result)) => { + // 任务成功完成 + process_result(result); + Ok(()) + } + Ok(Err(e)) => { + // 任务内部错误 + Err(e.into()) + } + Err(join_err) => { + // 任务 panic 或被取消 + if join_err.is_panic() { + error!("Task panicked: {:?}", join_err); + } + Err(anyhow!("Task failed: {}", join_err)) + } + } +} +``` + +### 结构化并发 vs spawn + +```rust +// ✅ 优先使用 join!(结构化并发) +async fn structured_concurrency() -> Result<(A, B, C)> { + // 所有任务在同一个作用域内 + // 如果任何一个失败,其他的会被取消 + tokio::try_join!( + fetch_a(), + fetch_b(), + fetch_c() + ) +} + +// ✅ 使用 spawn 时考虑任务生命周期 +struct TaskManager { + handles: Vec>, +} + +impl TaskManager { + async fn shutdown(self) { + // 优雅关闭:等待所有任务完成 + for handle in self.handles { + if let Err(e) = handle.await { + error!("Task failed during shutdown: {}", e); + } + } + } + + async fn abort_all(self) { + // 强制关闭:取消所有任务 + for handle in self.handles { + handle.abort(); + } + } +} ``` --- @@ -135,6 +583,41 @@ fn good_error() -> Result<()> { operation().context("failed to perform operation")?; Ok(()) } + +// ✅ 使用 with_context 进行懒计算 +fn good_error_lazy() -> Result<()> { + operation() + .with_context(|| format!("failed to process file: {}", filename))?; + Ok(()) +} +``` + +### 错误类型设计 + +```rust +// ✅ 使用 #[source] 保留错误链 +#[derive(Debug, thiserror::Error)] +pub enum ServiceError { + #[error("database error")] + Database(#[source] sqlx::Error), + + #[error("network error: {message}")] + Network { + message: String, + #[source] + source: reqwest::Error, + }, + + #[error("validation failed: {0}")] + Validation(String), +} + +// ✅ 为常见转换实现 From +impl From for ServiceError { + fn from(err: sqlx::Error) -> Self { + ServiceError::Database(err) + } +} ``` --- @@ -155,7 +638,7 @@ fn bad_sum(items: &[i32]) -> i32 { // ✅ 惰性迭代 fn good_sum(items: &[i32]) -> i32 { - items.iter().filter(|x| **x > 0).sum() + items.iter().filter(|x| **x > 0).copied().sum() } ``` @@ -173,7 +656,55 @@ fn bad_concat(items: &[&str]) -> String { // ✅ 预分配或用 join fn good_concat(items: &[&str]) -> String { - items.join("") // 或用 with_capacity + items.join("") +} + +// ✅ 使用 with_capacity 预分配 +fn good_concat_capacity(items: &[&str]) -> String { + let total_len: usize = items.iter().map(|s| s.len()).sum(); + let mut result = String::with_capacity(total_len); + for item in items { + result.push_str(item); + } + result +} + +// ✅ 使用 write! 宏 +use std::fmt::Write; + +fn good_concat_write(items: &[&str]) -> String { + let mut result = String::new(); + for item in items { + write!(result, "{}", item).unwrap(); + } + result +} +``` + +### 避免不必要的分配 + +```rust +// ❌ 不必要的 Vec 分配 +fn bad_check_any(items: &[Item]) -> bool { + let filtered: Vec<_> = items.iter() + .filter(|i| i.is_valid()) + .collect(); + !filtered.is_empty() +} + +// ✅ 使用迭代器方法 +fn good_check_any(items: &[Item]) -> bool { + items.iter().any(|i| i.is_valid()) +} + +// ❌ String::from 用于静态字符串 +fn bad_static() -> String { + String::from("error message") // 运行时分配 +} + +// ✅ 返回 &'static str +fn good_static() -> &'static str { + "error message" // 无分配 } ``` @@ -181,6 +712,8 @@ fn good_concat(items: &[&str]) -> String { ## Trait 设计 +### 避免过度抽象 + ```rust // ❌ 过度抽象——不是 Java,不需要 Interface 一切 trait Processor { fn process(&self); } @@ -189,6 +722,39 @@ trait Manager { fn manage(&self); } // Trait 过多 // ✅ 只在需要多态时创建 trait // 具体类型通常更简单、更快 +struct DataProcessor { + config: Config, +} + +impl DataProcessor { + fn process(&self, data: &Data) -> Result { + // 直接实现 + } +} +``` + +### Trait 对象 vs 泛型 + +```rust +// ❌ 不必要的 trait 对象(动态分发) +fn bad_process(handler: &dyn Handler) { + handler.handle(); // 虚表调用 +} + +// ✅ 使用泛型(静态分发,可内联) +fn good_process(handler: &H) { + handler.handle(); // 可能被内联 +} + +// ✅ trait 对象适用场景:异构集合 +fn store_handlers(handlers: Vec>) { + // 需要存储不同类型的 handlers +} + +// ✅ 使用 impl Trait 返回类型 +fn create_handler() -> impl Handler { + ConcreteHandler::new() +} ``` --- @@ -213,6 +779,7 @@ trait Manager { fn manage(&self); } // Trait 过多 - [ ] Arc> 真的需要共享状态吗? - [ ] RefCell 的使用有正当理由 - [ ] 生命周期不过度复杂 +- [ ] 考虑使用 Cow 避免不必要的分配 ### Unsafe 代码(最重要) @@ -231,6 +798,21 @@ trait Manager { fn manage(&self); } // Trait 过多 - [ ] 锁的获取顺序一致 - [ ] Channel 缓冲区大小合理 +### 取消安全性 + +- [ ] select! 中的 Future 是取消安全的 +- [ ] 文档化了 async 函数的取消安全性 +- [ ] 取消不会导致数据丢失或不一致状态 +- [ ] 使用 tokio::pin! 正确处理需要重用的 Future + +### spawn vs await + +- [ ] spawn 只用于真正需要并行的场景 +- [ ] 简单操作直接 await,不要 spawn +- [ ] spawn 的 JoinHandle 结果被正确处理 +- [ ] 考虑任务的生命周期和关闭策略 +- [ ] 优先使用 join!/try_join! 进行结构化并发 + ### 错误处理 - [ ] 库:thiserror 定义结构化错误 @@ -238,6 +820,7 @@ trait Manager { fn manage(&self); } // Trait 过多 - [ ] 没有生产代码 unwrap/expect - [ ] 错误消息对调试有帮助 - [ ] must_use 返回值被处理 +- [ ] 使用 #[source] 保留错误链 ### 性能 @@ -246,6 +829,7 @@ trait Manager { fn manage(&self); } // Trait 过多 - [ ] 字符串用 with_capacity 或 write! - [ ] impl Trait vs Box 选择合理 - [ ] 热路径避免分配 +- [ ] 考虑使用 Cow 减少克隆 ### 代码质量 diff --git a/reference/typescript.md b/reference/typescript.md index 9d204d4..4699f6b 100644 --- a/reference/typescript.md +++ b/reference/typescript.md @@ -1,53 +1,405 @@ # TypeScript/JavaScript Code Review Guide -> TypeScript/JavaScript 通用代码审查指南,覆盖类型安全、异步处理、常见陷阱。 +> TypeScript 代码审查指南,覆盖类型系统、泛型、条件类型、strict 模式、async/await 模式等核心主题。 + +## 目录 + +- [类型安全基础](#类型安全基础) +- [泛型模式](#泛型模式) +- [高级类型](#高级类型) +- [Strict 模式配置](#strict-模式配置) +- [异步处理](#异步处理) +- [不可变性](#不可变性) +- [ESLint 规则](#eslint-规则) +- [Review Checklist](#review-checklist) --- -## 类型安全 +## 类型安全基础 ### 避免使用 any ```typescript // ❌ Using any defeats type safety -function processData(data: any) { // Avoid any - return data.value; +function processData(data: any) { + return data.value; // 无类型检查,运行时可能崩溃 } // ✅ Use proper types interface DataPayload { - value: string; + value: string; } function processData(data: DataPayload) { - return data.value; + return data.value; } + +// ✅ 未知类型用 unknown + 类型守卫 +function processUnknown(data: unknown) { + if (typeof data === 'object' && data !== null && 'value' in data) { + return (data as { value: string }).value; + } + throw new Error('Invalid data'); +} +``` + +### 类型收窄 + +```typescript +// ❌ 不安全的类型断言 +function getLength(value: string | string[]) { + return (value as string[]).length; // 如果是 string 会出错 +} + +// ✅ 使用类型守卫 +function getLength(value: string | string[]): number { + if (Array.isArray(value)) { + return value.length; + } + return value.length; +} + +// ✅ 使用 in 操作符 +interface Dog { bark(): void } +interface Cat { meow(): void } + +function speak(animal: Dog | Cat) { + if ('bark' in animal) { + animal.bark(); + } else { + animal.meow(); + } +} +``` + +### 字面量类型与 as const + +```typescript +// ❌ 类型过于宽泛 +const config = { + endpoint: '/api', + method: 'GET' // 类型是 string +}; + +// ✅ 使用 as const 获得字面量类型 +const config = { + endpoint: '/api', + method: 'GET' +} as const; // method 类型是 'GET' + +// ✅ 用于函数参数 +function request(method: 'GET' | 'POST', url: string) { ... } +request(config.method, config.endpoint); // 正确! +``` + +--- + +## 泛型模式 + +### 基础泛型 + +```typescript +// ❌ 重复代码 +function getFirstString(arr: string[]): string | undefined { + return arr[0]; +} +function getFirstNumber(arr: number[]): number | undefined { + return arr[0]; +} + +// ✅ 使用泛型 +function getFirst(arr: T[]): T | undefined { + return arr[0]; +} +``` + +### 泛型约束 + +```typescript +// ❌ 泛型没有约束,无法访问属性 +function getProperty(obj: T, key: string) { + return obj[key]; // Error: 无法索引 +} + +// ✅ 使用 keyof 约束 +function getProperty(obj: T, key: K): T[K] { + return obj[key]; +} + +const user = { name: 'Alice', age: 30 }; +getProperty(user, 'name'); // 返回类型是 string +getProperty(user, 'age'); // 返回类型是 number +getProperty(user, 'foo'); // Error: 'foo' 不在 keyof User +``` + +### 泛型默认值 + +```typescript +// ✅ 提供合理的默认类型 +interface ApiResponse { + data: T; + status: number; + message: string; +} + +// 可以不指定泛型参数 +const response: ApiResponse = { data: null, status: 200, message: 'OK' }; +// 也可以指定 +const userResponse: ApiResponse = { ... }; +``` + +### 常见泛型工具类型 + +```typescript +// ✅ 善用内置工具类型 +interface User { + id: number; + name: string; + email: string; +} + +type PartialUser = Partial; // 所有属性可选 +type RequiredUser = Required; // 所有属性必需 +type ReadonlyUser = Readonly; // 所有属性只读 +type UserKeys = keyof User; // 'id' | 'name' | 'email' +type NameOnly = Pick; // { name: string } +type WithoutId = Omit; // { name: string; email: string } +type UserRecord = Record; // { [key: string]: User } +``` + +--- + +## 高级类型 + +### 条件类型 + +```typescript +// ✅ 根据输入类型返回不同类型 +type IsString = T extends string ? true : false; + +type A = IsString; // true +type B = IsString; // false + +// ✅ 提取数组元素类型 +type ElementType = T extends (infer U)[] ? U : never; + +type Elem = ElementType; // string + +// ✅ 提取函数返回类型(内置 ReturnType) +type MyReturnType = T extends (...args: any[]) => infer R ? R : never; +``` + +### 映射类型 + +```typescript +// ✅ 转换对象类型的所有属性 +type Nullable = { + [K in keyof T]: T[K] | null; +}; + +interface User { + name: string; + age: number; +} + +type NullableUser = Nullable; +// { name: string | null; age: number | null } + +// ✅ 添加前缀 +type Getters = { + [K in keyof T as `get${Capitalize}`]: () => T[K]; +}; + +type UserGetters = Getters; +// { getName: () => string; getAge: () => number } +``` + +### 模板字面量类型 + +```typescript +// ✅ 类型安全的事件名称 +type EventName = 'click' | 'focus' | 'blur'; +type HandlerName = `on${Capitalize}`; +// 'onClick' | 'onFocus' | 'onBlur' + +// ✅ API 路由类型 +type ApiRoute = `/api/${string}`; +const route: ApiRoute = '/api/users'; // OK +const badRoute: ApiRoute = '/users'; // Error +``` + +### Discriminated Unions + +```typescript +// ✅ 使用判别属性实现类型安全 +type Result = + | { success: true; data: T } + | { success: false; error: E }; + +function handleResult(result: Result) { + if (result.success) { + console.log(result.data.name); // TypeScript 知道 data 存在 + } else { + console.log(result.error.message); // TypeScript 知道 error 存在 + } +} + +// ✅ Redux Action 模式 +type Action = + | { type: 'INCREMENT'; payload: number } + | { type: 'DECREMENT'; payload: number } + | { type: 'RESET' }; + +function reducer(state: number, action: Action): number { + switch (action.type) { + case 'INCREMENT': + return state + action.payload; // payload 类型已知 + case 'DECREMENT': + return state - action.payload; + case 'RESET': + return 0; // 这里没有 payload + } +} +``` + +--- + +## Strict 模式配置 + +### 推荐的 tsconfig.json + +```json +{ + "compilerOptions": { + // ✅ 必须开启的 strict 选项 + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "useUnknownInCatchVariables": true, + + // ✅ 额外推荐选项 + "noUncheckedIndexedAccess": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "exactOptionalPropertyTypes": true, + "noPropertyAccessFromIndexSignature": true + } +} +``` + +### noUncheckedIndexedAccess 的影响 + +```typescript +// tsconfig: "noUncheckedIndexedAccess": true + +const arr = [1, 2, 3]; +const first = arr[0]; // 类型是 number | undefined + +// ❌ 直接使用可能出错 +console.log(first.toFixed(2)); // Error: 可能是 undefined + +// ✅ 先检查 +if (first !== undefined) { + console.log(first.toFixed(2)); +} + +// ✅ 或使用非空断言(确定时) +console.log(arr[0]!.toFixed(2)); ``` --- ## 异步处理 -### 错误处理 +### Promise 错误处理 ```typescript // ❌ Not handling async errors async function fetchUser(id: string) { - const response = await fetch(`/api/users/${id}`); - return response.json(); // What if network fails? + const response = await fetch(`/api/users/${id}`); + return response.json(); // 网络错误未处理 } // ✅ Handle errors properly async function fetchUser(id: string): Promise { - try { - const response = await fetch(`/api/users/${id}`); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - return await response.json(); - } catch (error) { - console.error('Failed to fetch user:', error); - throw error; + try { + const response = await fetch(`/api/users/${id}`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); } + return await response.json(); + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to fetch user: ${error.message}`); + } + throw error; + } +} +``` + +### Promise.all vs Promise.allSettled + +```typescript +// ❌ Promise.all 一个失败全部失败 +async function fetchAllUsers(ids: string[]) { + const users = await Promise.all(ids.map(fetchUser)); + return users; // 一个失败就全部失败 +} + +// ✅ Promise.allSettled 获取所有结果 +async function fetchAllUsers(ids: string[]) { + const results = await Promise.allSettled(ids.map(fetchUser)); + + const users: User[] = []; + const errors: Error[] = []; + + for (const result of results) { + if (result.status === 'fulfilled') { + users.push(result.value); + } else { + errors.push(result.reason); + } + } + + return { users, errors }; +} +``` + +### 竞态条件处理 + +```typescript +// ❌ 竞态条件:旧请求可能覆盖新请求 +function useSearch() { + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + + useEffect(() => { + fetch(`/api/search?q=${query}`) + .then(r => r.json()) + .then(setResults); // 旧请求可能后返回! + }, [query]); +} + +// ✅ 使用 AbortController +function useSearch() { + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + + useEffect(() => { + const controller = new AbortController(); + + fetch(`/api/search?q=${query}`, { signal: controller.signal }) + .then(r => r.json()) + .then(setResults) + .catch(e => { + if (e.name !== 'AbortError') throw e; + }); + + return () => controller.abort(); + }, [query]); } ``` @@ -55,47 +407,137 @@ async function fetchUser(id: string): Promise { ## 不可变性 -### 避免直接修改 Props/参数 +### Readonly 与 ReadonlyArray ```typescript -// ❌ Mutation of props -function UserProfile({ user }: Props) { - user.lastViewed = new Date(); // Mutating prop! - return
{user.name}
; +// ❌ 可变参数可能被意外修改 +function processUsers(users: User[]) { + users.sort((a, b) => a.name.localeCompare(b.name)); // 修改了原数组! + return users; } -// ✅ Don't mutate props -function UserProfile({ user, onView }: Props) { - useEffect(() => { - onView(user.id); // Notify parent to update - }, [user.id]); - return
{user.name}
; +// ✅ 使用 readonly 防止修改 +function processUsers(users: readonly User[]): User[] { + return [...users].sort((a, b) => a.name.localeCompare(b.name)); } + +// ✅ 深度只读 +type DeepReadonly = { + readonly [K in keyof T]: T[K] extends object ? DeepReadonly : T[K]; +}; +``` + +### 不变式函数参数 + +```typescript +// ✅ 使用 as const 和 readonly 保护数据 +function createConfig(routes: T) { + return routes; +} + +const routes = createConfig(['home', 'about', 'contact'] as const); +// 类型是 readonly ['home', 'about', 'contact'] ``` --- -## TypeScript Review Checklist +## ESLint 规则 + +### 推荐的 @typescript-eslint 规则 + +```javascript +// .eslintrc.js +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'plugin:@typescript-eslint/strict' + ], + rules: { + // ✅ 类型安全 + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-unsafe-assignment': 'error', + '@typescript-eslint/no-unsafe-member-access': 'error', + '@typescript-eslint/no-unsafe-call': 'error', + '@typescript-eslint/no-unsafe-return': 'error', + + // ✅ 最佳实践 + '@typescript-eslint/explicit-function-return-type': 'warn', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-misused-promises': 'error', + + // ✅ 代码风格 + '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/prefer-nullish-coalescing': 'error', + '@typescript-eslint/prefer-optional-chain': 'error' + } +}; +``` + +### 常见 ESLint 错误修复 + +```typescript +// ❌ no-floating-promises: Promise 必须被处理 +async function save() { ... } +save(); // Error: 未处理的 Promise + +// ✅ 显式处理 +await save(); +// 或 +save().catch(console.error); +// 或明确忽略 +void save(); + +// ❌ no-misused-promises: 不能在非 async 位置使用 Promise +const items = [1, 2, 3]; +items.forEach(async (item) => { // Error! + await processItem(item); +}); + +// ✅ 使用 for...of +for (const item of items) { + await processItem(item); +} +// 或 Promise.all +await Promise.all(items.map(processItem)); +``` + +--- + +## Review Checklist ### 类型系统 -- [ ] 没有使用 `any`(使用 `unknown` 代替未知类型) -- [ ] 接口和类型定义完整 +- [ ] 没有使用 `any`(使用 `unknown` + 类型守卫代替) +- [ ] 接口和类型定义完整且有意义的命名 - [ ] 使用泛型提高代码复用性 - [ ] 联合类型有正确的类型收窄 +- [ ] 善用工具类型(Partial、Pick、Omit 等) + +### 泛型 +- [ ] 泛型有适当的约束(extends) +- [ ] 泛型参数有合理的默认值 +- [ ] 避免过度泛型化(KISS 原则) + +### Strict 模式 +- [ ] tsconfig.json 启用了 strict: true +- [ ] 启用了 noUncheckedIndexedAccess +- [ ] 没有使用 @ts-ignore(改用 @ts-expect-error) ### 异步代码 - [ ] async 函数有错误处理 - [ ] Promise rejection 被正确处理 -- [ ] 避免 callback hell,使用 async/await -- [ ] 并发请求使用 `Promise.all` 或 `Promise.allSettled` +- [ ] 没有 floating promises(未处理的 Promise) +- [ ] 并发请求使用 Promise.all 或 Promise.allSettled +- [ ] 竞态条件使用 AbortController 处理 ### 不可变性 - [ ] 不直接修改函数参数 - [ ] 使用 spread 操作符创建新对象/数组 -- [ ] 考虑使用 `readonly` 修饰符 +- [ ] 考虑使用 readonly 修饰符 -### 代码质量 -- [ ] 启用 strict 模式 -- [ ] ESLint/TSLint 无警告 -- [ ] 函数有返回类型注解 -- [ ] 避免类型断言(`as`),除非确实必要 +### ESLint +- [ ] 使用 @typescript-eslint/recommended +- [ ] 没有 ESLint 警告或错误 +- [ ] 使用 consistent-type-imports diff --git a/reference/vue.md b/reference/vue.md index 3b3f71e..649c3e6 100644 --- a/reference/vue.md +++ b/reference/vue.md @@ -1,11 +1,54 @@ # Vue 3 Code Review Guide -> Vue 3 Composition API 代码审查指南,覆盖响应性系统、Props/Emits、Watchers、Composables 等核心主题。 +> Vue 3 Composition API 代码审查指南,覆盖响应性系统、Props/Emits、Watchers、Composables、Vue 3.5 新特性等核心主题。 + +## 目录 + +- [响应性系统](#响应性系统) +- [Props & Emits](#props--emits) +- [Vue 3.5 新特性](#vue-35-新特性) +- [Watchers](#watchers) +- [模板最佳实践](#模板最佳实践) +- [Composables](#composables) +- [性能优化](#性能优化) +- [Review Checklist](#review-checklist) --- ## 响应性系统 +### ref vs reactive 选择 + +```vue + + + + + + + + +``` + ### 解构 reactive 对象 ```vue @@ -49,6 +92,33 @@ watch(fullName, (name) => { ``` +### shallowRef 优化 + +```vue + + + + + +``` + --- ## Props & Emits @@ -94,10 +164,274 @@ const props = withDefaults(defineProps(), { ``` +### defineEmits 类型安全 + +```vue + + + + + +``` + +--- + +## Vue 3.5 新特性 + +### Reactive Props Destructure (3.5+) + +```vue + + + + + + + + +``` + +### defineModel (3.4+) + +```vue + + + + + + + + + + + + + + + + +``` + +### useTemplateRef (3.5+) + +```vue + + + + + + + + + + +``` + +### useId (3.5+) + +```vue + + + + + + + + + + +``` + +### onWatcherCleanup (3.5+) + +```vue + + + + + +``` + +### Deferred Teleport (3.5+) + +```vue + + + + + +``` + --- ## Watchers +### watch vs watchEffect + +```vue + +``` + ### watch 清理函数 ```vue @@ -131,6 +465,71 @@ watch(searchQuery, async (query, _, onCleanup) => { ``` +### watch 选项 + +```vue + +``` + +### 监听多个源 + +```vue + +``` + --- ## 模板最佳实践 @@ -151,6 +550,13 @@ watch(searchQuery, async (query, _, onCleanup) => { {{ item.name }} + + + ``` ### v-if 和 v-for 优先级 @@ -174,12 +580,87 @@ const activeUsers = computed(() => {{ user.name }} + + + +``` + +### 事件处理 + +```vue + + + + + + + + + ``` --- ## Composables +### Composable 设计原则 + +```typescript +// ✅ 好的 composable 设计 +export function useCounter(initialValue = 0) { + const count = ref(initialValue) + + const increment = () => count.value++ + const decrement = () => count.value-- + const reset = () => count.value = initialValue + + // 返回响应式引用和方法 + return { + count: readonly(count), // 只读防止外部修改 + increment, + decrement, + reset + } +} + +// ❌ 不要返回 .value +export function useBadCounter() { + const count = ref(0) + return { + count: count.value // ❌ 丢失响应性! + } +} +``` + ### Props 传递给 composable ```vue @@ -196,35 +677,236 @@ const userIdRef = toRef(props, 'userId') const { user } = useUser(userIdRef) // 保持响应性 // 或使用 computed const { user } = useUser(computed(() => props.userId)) + +// ✅ Vue 3.5+:直接解构使用 +const { userId } = defineProps<{ userId: string }>() +const { user } = useUser(() => userId) // getter 函数 ``` +### 异步 Composable + +```typescript +// ✅ 异步 composable 模式 +export function useFetch(url: MaybeRefOrGetter) { + const data = ref(null) + const error = ref(null) + const loading = ref(false) + + const execute = async () => { + loading.value = true + error.value = null + + try { + const response = await fetch(toValue(url)) + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) + } + data.value = await response.json() + } catch (e) { + error.value = e as Error + } finally { + loading.value = false + } + } + + // 响应式 URL 时自动重新获取 + watchEffect(() => { + toValue(url) // 追踪依赖 + execute() + }) + + return { + data: readonly(data), + error: readonly(error), + loading: readonly(loading), + refetch: execute + } +} + +// 使用 +const { data, loading, error, refetch } = useFetch('/api/users') +``` + +### 生命周期与清理 + +```typescript +// ✅ Composable 中正确处理生命周期 +export function useEventListener( + target: MaybeRefOrGetter, + event: string, + handler: EventListener +) { + // 组件挂载后添加 + onMounted(() => { + toValue(target).addEventListener(event, handler) + }) + + // 组件卸载时移除 + onUnmounted(() => { + toValue(target).removeEventListener(event, handler) + }) +} + +// ✅ 使用 effectScope 管理副作用 +export function useFeature() { + const scope = effectScope() + + scope.run(() => { + // 所有响应式效果都在这个 scope 内 + const state = ref(0) + watch(state, () => { /* ... */ }) + watchEffect(() => { /* ... */ }) + }) + + // 清理所有效果 + onUnmounted(() => scope.stop()) + + return { /* ... */ } +} +``` + --- -## Vue 3 Review Checklist +## 性能优化 + +### v-memo + +```vue + + + + + +``` + +### defineAsyncComponent + +```vue + +``` + +### KeepAlive + +```vue + + + +``` + +### 虚拟列表 + +```vue + + + +``` + +--- + +## Review Checklist ### 响应性系统 -- [ ] ref 用于基本类型,reactive 用于对象 +- [ ] ref 用于基本类型,reactive 用于对象(或统一用 ref) - [ ] 没有解构 reactive 对象(或使用了 toRefs) - [ ] props 传递给 composable 时保持了响应性 - [ ] shallowRef/shallowReactive 用于大型对象优化 +- [ ] computed 中没有副作用 ### Props & Emits - [ ] defineProps 使用 TypeScript 类型声明 - [ ] 复杂默认值使用 withDefaults + 工厂函数 - [ ] defineEmits 有完整的类型定义 - [ ] 没有直接修改 props +- [ ] 考虑使用 defineModel 简化 v-model(Vue 3.4+) + +### Vue 3.5 新特性(如适用) +- [ ] 使用 Reactive Props Destructure 简化 props 访问 +- [ ] 使用 useTemplateRef 替代 ref 属性 +- [ ] 表单使用 useId 生成 SSR 安全的 ID +- [ ] 使用 onWatcherCleanup 处理复杂清理逻辑 ### Watchers - [ ] watch/watchEffect 有适当的清理函数 - [ ] 异步 watch 处理了竞态条件 - [ ] flush: 'post' 用于 DOM 操作的 watcher - [ ] 避免过度使用 watcher(优先用 computed) +- [ ] 考虑 once: true 用于一次性监听 ### 模板 - [ ] v-for 使用唯一且稳定的 key - [ ] v-if 和 v-for 没有在同一元素上 -- [ ] 事件处理使用 kebab-case +- [ ] 事件处理使用方法而非内联复杂逻辑 - [ ] 大型列表使用虚拟滚动 ### Composables @@ -232,9 +914,11 @@ const { user } = useUser(computed(() => props.userId)) - [ ] composables 返回响应式引用(不是 .value) - [ ] 纯函数不要包装成 composable - [ ] 副作用在组件卸载时清理 +- [ ] 使用 effectScope 管理复杂副作用 ### 性能 - [ ] 大型组件拆分为小组件 - [ ] 使用 defineAsyncComponent 懒加载 - [ ] 避免不必要的响应式转换 - [ ] v-memo 用于昂贵的列表渲染 +- [ ] KeepAlive 用于缓存动态组件