---
title: "Refactory：懂你專案慣例的 VS Code 重構外掛，完整功能教學"
description: "VS Code 少了一味好用的重構。Refactory 補上這個洞：34 個重構動作、38 條壞味道檢查、Code Health 儀表板、18 種語言，還能一鍵把問題交給本機 Claude Code 修。這篇是操作教學，每個功能只講三件事：游標放哪、按什麼鍵、按完會變成什麼樣子。免費使用。"
canonical_url: "https://blog.markkulab.net/post/vscode-refactory-code-health"
author: "Mark Ku"
author_url: "https://blog.markkulab.net/author/mark-ku"
site: "Mark Ku's Blog"
date_published: "2026-07-31 02:40:00 +0800"
category: "Tech Sharing"
tags: ["vscode", "extension", "refactoring", "csharp", "react", "nextjs", "roslyn", "typescript", "javascript", "python", "go", "rust"]
language: "zh-TW"
license: "CC BY 4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
attribution: "轉載或引用請註明作者並附上原文連結"
---

# Refactory：懂你專案慣例的 VS Code 重構外掛，完整功能教學

> 📖 [功能介紹頁](https://blog.markkulab.net/tools/refactory)　｜　⬇️ [從 VS Code Marketplace 安裝](https://marketplace.visualstudio.com/items?itemName=mark-ku.refactory)
>
> 免費使用，VS Code 內按 `Ctrl+P` 貼上 `ext install mark-ku.refactory` 也可直接安裝。

## 前言

把主力開發搬進 VS Code 之後，最不習慣的是**重構**（不改變程式行為，只把它整理得更好讀）。

在傳統商業 IDE 裡按一個鍵，這個位置能做的事會全部列出來。到了 VS Code，這些功能散在各處，而且少了一大半。就算有，**產生出來的碼也不像你專案的碼**：介面該放哪個資料夾？DI 註冊寫在哪？`'use client'` 該不該帶過去？通用工具都不知道。

所以我做了 **Refactory**，已上架 [Marketplace](https://marketplace.visualstudio.com/items?itemName=mark-ku.refactory)，免費使用，**34 個重構動作 + 38 條壞味道檢查、18 種語言**。

這篇只講**怎麼操作**：游標放哪、按什麼鍵、按完變成什麼樣子。

## 一、安裝（30 秒）

VS Code 內按 `Ctrl+P` 貼上 `ext install mark-ku.refactory`，或在 Extensions 面板搜尋 `Refactory`。裝完就能用，**不用設定**。

要用 C# 功能的話電腦要有 **.NET 8 runtime**；沒裝只會關掉 C# 那一半，React / TypeScript 不受影響。

## 二、只要記一組快速鍵

| 按鍵 | 什麼時候按 |
|---|---|
| **`Ctrl+Alt+Shift+T`** | **主力**。把游標所在位置能做的事全部列出來 |
| `Ctrl+.` | 原生燈泡選單，Refactory 的動作也嵌在裡面 |
| `Ctrl+Shift+P` → 打 `Refactory` | 對「整個檔案」操作的指令 |
| 編輯器裡按右鍵 | 選單裡也有一項「重構這裡…」，跟第一列同一個東西 |

記住第一個就夠了。

![Refactor This 聚合選單，Refactory 與內建的重構並列在同一個選單](https://blog.markkulab.net/content/markku/posts/vscode-refactory-code-health/images/01-refactor-this-menu-v2.png)

兩個會幫到你的地方：**最近用過的動作會浮到最上面**；**不能用的動作也會列出來並告訴你原因**，不用猜。

習慣按 `Ctrl+.` 的人不用改習慣，動作一樣找得到：

![原生 refactor 選單，Refactory 的動作正確落在 Extract、Rewrite、Move 分組內](https://blog.markkulab.net/content/markku/posts/vscode-refactory-code-health/images/02-extract-component-lightbulb-v2.png)

## 三、React / Next.js

### 3-1 把一段 JSX 抽成元件

![Extract Component 實機錄影：選取 JSX、抽出元件、原地命名](https://blog.markkulab.net/content/markku/posts/vscode-refactory-code-health/images/06-demo-extract-component.gif)

> **怎麼操作**
> 1. 選取一段**完整的** JSX 區塊
> 2. 按 `Ctrl+Alt+Shift+T`
> 3. 選 `Extract JSX into a component`（同檔）或 `…into a new component file`（新檔）
> 4. 打名字，Enter

**選取這段** → **它產生這樣**（props 型別自動推導，不用自己填）：

**選取的JSX**

```tsx
<section className="hero">
  <h1>{active}</h1>
  <button onClick={() => setActive('next')}>Next</button>
</section>
```

**抽出來的元件**

```tsx
interface NewComponentProps {
  active: string
  setActive: (value: string) => void
}

function NewComponent({ active, setActive }: NewComponentProps) {
  return (
    <section className="hero">
      <h1>{active}</h1>
      <button onClick={() => setActive('next')}>Next</button>
    </section>
  )
}
```

原本的位置自動換成 `<NewComponent active={active} setActive={setActive} />`。

![Extract Component 執行結果，四處元件名稱同時被框選](https://blog.markkulab.net/content/markku/posts/vscode-refactory-code-health/images/03-extract-component-result-v2.png)

注意狀態列的 **4 selections**：呼叫點、interface 名稱、函式名稱、型別引用同時進入編輯狀態，**打一次字四處一起改**。

抽到**新檔案**時會順手做這些：`'use client'` 帶過去、只搬用到的 import、共用常數改成 export + import（不是複製一份）、檔案位置與命名照你專案的慣例。

跨檔案操作會先跳 **Refactor Preview 面板**，逐條確認後才寫入。

### 3-2 把一坨 state 邏輯抽成自訂 hook

> **怎麼操作**：選取**連續的幾行**（至少含一個 hook 呼叫）→ `Ctrl+Alt+Shift+T` → `Extract into a custom hook` → 打名字

**選取的幾行**

```tsx
const [query, setQuery] = useState('')
const [results, setResults] = useState<Post[]>([])
useEffect(() => {
  if (!query) return
  fetchPosts(query).then(setResults)
}, [query])
```

**抽出來的hook**

```tsx
function useSearch() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<Post[]>([])
  useEffect(() => {
    if (!query) return
    fetchPosts(query).then(setResults)
  }, [query])

  return { query, setQuery, results }
}
```

**`useEffect` 的依賴陣列絕對不會被動到**。搬完覺得依賴怪怪的，代表這段本來就有問題，那是另一個 commit 的事。

### 3-3 把 JSX 包起來：條件、`.map()`、fragment

三個都是選取一段完整 JSX 之後按 `Ctrl+Alt+Shift+T`，差別只在選單挑哪一項。

**包成條件顯示**：

**選取的 JSX**

```tsx
<div>
  <b>{n}</b>
</div>
```

**Wrap in a conditional**

```tsx
<div>
  {condition && (
    <b>{n}</b>
  )}
</div>
```

**包成 `.map()`**：

**選取的 JSX**

```tsx
<ul>
  <li>{name}</li>
</ul>
```

**Wrap in a .map()**

```tsx
<ul>
  {items.map((item) => (
    <li key={item}>{name}</li>
  ))}
</ul>
```

`condition`、`items`、`item` 都是**就地可編輯的游標點**，Tab 跳下一個，打字直接改名，不用回頭找。

`key={item}` 是先幫你補上去、**同時附一條警告**：沒有人知道你的資料拿什麼當唯一鍵，所以它給一個編得過的預設值並明說「請換成穩定 id」，而不是安靜地留一個空位，讓 React 事後才在 console 抱怨。

選**兩個以上的兄弟節點**時，選單改成提供 fragment（`<>…</>`）；只選一個根節點時才會出現 `.map()`——包單一節點成 fragment 沒有意義，而 `.map()` 一次包兩個節點會產生無效的 JSX。

### 3-4 `const X: FC<Props>` 轉成函式宣告

> **怎麼操作**：游標放在元件宣告那一行 → `Ctrl+Alt+Shift+T` → 選轉成函式宣告

**箭頭函式 + FC**

```tsx
interface Props {
  title: string
}

const Card: FC<Props> = ({ title }) => {
  return <b>{title}</b>
}

export default Card
```

**函式宣告**

```tsx
interface Props {
  title: string
}

export default function Card({ title }: Props) {
  return <b>{title}</b>
}
```

兩個細節：型別從**變數**搬到**參數**上；下面那行單獨的 `export default Card` 被吸收掉，不會留一行孤兒。簡寫的 body（`=> <b>{title}</b>`）會補成 `return`。

這個動作**一定會附一條警告**：`FC` 隱含一個 `children` prop，轉成函式宣告之後就沒有了。如果你的元件其實有人塞 children 進來，型別會從這一刻開始才真的檢查你。泛型元件（`<T,>`）則直接拒絕，因為 `FC<Props>` 本來就寫不出泛型。

### 3-5 改一個 prop 的名字，連所有呼叫點一起改

這是純手工最容易漏的一種：改了型別、改了解構，漏掉別的檔案裡的 `<Card title="…" />`。

> **怎麼操作**：游標放在 **props 型別裡的那個屬性**上 → `Ctrl+Alt+Shift+T` → 選改 prop 名稱 → 打新名字

**Card.tsx（title）**

```tsx
interface CardProps {
  title: string
  count: number
}

export function Card({ title, count }: CardProps) {
  return <b>{title}: {count}</b>
}
```

**Card.tsx（heading）**

```tsx
interface CardProps {
  heading: string
  count: number
}

export function Card({ heading, count }: CardProps) {
  return <b>{heading}: {count}</b>
}
```

同一次動作也會把**別的檔案**裡的呼叫點改掉：

**Panel.tsx（改之前）**

```tsx
export function Panel() {
  return <Card title="hello" count={1} />
}
```

**Panel.tsx（改之後）**

```tsx
export function Panel() {
  return <Card heading="hello" count={1} />
}
```

旁邊的 `count` 一個字都沒動。解構時本來就寫了別名（`{ title: heading }`）的話，它只改型別成員，不會去動你已經取好的區域名稱。

**遇到 `<Card {...props} />` 這種展開的呼叫點會直接拒絕**：那個物件裡到底有沒有 `title`，要靠型別推導才知道，一個 lex 層級的索引不該假裝自己知道。

### 3-6 Inline variable 與 import 路徑轉換

**Inline variable**（把只用一次的中間變數就地展開）：

**展開前**

```tsx
export function Panel() {
  const label = 'hello'
  return <b>{label}</b>
}
```

**展開後**

```tsx
export function Panel() {
  return <b>{'hello'}</b>
}
```

用了兩次以上、而且初始化沒有副作用時會**全部**展開，並在需要的地方補上括號保住運算優先序（`n * 2` 展開成 `{n * 2}{n * 2}`）。三種情況它會拒絕：`let`（值會變）、初始化有副作用又被用了不只一次（會多跑幾次），還有**被 `useEffect` 依賴陣列點名的變數**（展開等於偷偷改了依賴）。

**import 路徑轉換**（游標放在路徑字串上）：

**相對路徑**

```ts
import { posts } from '../../data/posts'
import type { TPost } from '../../data/types'
```

**alias 路徑**

```ts
import { posts } from '@/data/posts'
import type { TPost } from '@/data/types'
```

alias 是從**管這個檔案的那份 `tsconfig.json`** 的 `paths` 讀出來的，不用另外設定；`import type`、`export … from`、動態 `import()`、純副作用 import 都認得，原本用單引號或雙引號也照舊。兩個方向可以來回轉，轉回去會拿到一模一樣的原文。

### 3-7 游標放哪：完整對照表

**游標放錯位置是這類工具最常卡的地方**，直接查這張表：

| 想做的事 | 游標 / 選取位置 |
|---|---|
| 抽 JSX 成元件 | 選取一段**完整的** JSX 區塊 |
| 抽自訂 hook | 選取**連續的幾行**，至少含一個 hook 呼叫 |
| `const X: FC<Props>` 轉函式宣告 | 放在元件宣告上 |
| 包 fragment / 條件 / `.map()` | 選取一段完整的 JSX 區塊 |
| Inline variable（變數就地展開） | 放在 `const` 宣告或它的任一引用上 |
| 單一 import 轉 alias / 相對路徑 | 放在路徑字串上（如 `'../../data/posts'`） |
| **整檔** import 一次轉換 | `Ctrl+Shift+P` → *Refactory: TypeScript: Convert Imports…* |
| 把宣告搬到獨立檔案 | 放在**被宣告的那個名稱**上（引用處會一起改） |
| Safe delete（安全刪除） | 放在被宣告的名稱上；有人在用會拒絕並列出誰在用 |
| 改 prop 名稱（含所有呼叫點） | 放在 props 型別裡的那個屬性上 |
| `'use client'` 修正 | 不用操作，會出現在 **Problems** 面板，`Ctrl+.` 套用 |

三個提醒：**JavaScript 也適用**（產出不帶型別註記，副檔名跟著來源走）；import alias 不用設定，直接從管這個檔的 `tsconfig.json` 讀；整檔轉換刻意只放命令面板，避免游標一動就跳出來。

### 3-8 `'use client'` 自動提醒（Next.js）

**不用操作**，在 Next.js 專案裡打字就會出現：

| 情況 | 它怎麼處理 |
|---|---|
| 用了 `useState` 卻忘了加 directive | 出現警告，`Ctrl+.` → **Add 'use client' directive** |
| 加了，但同檔又 export 只能跑在伺服器的東西 | **不給快速修復**，並直說：要把 client 的部分拆成獨立元件，不是把邊界往上搬 |
| 加了但根本用不到 | 最低等級的提示，拿掉就不會被打包進瀏覽器 |

### 3-9 React 壞味道即時提醒

「壞味道」＝**編得過、但之後會讓你痛**的寫法。打字時就畫波浪線，每條都附 `Ctrl+.` 快速修復。

![React 壞味道即時偵測與一鍵修復的實機錄影](https://blog.markkulab.net/content/markku/posts/vscode-refactory-code-health/images/10-demo-react-smells-fix.gif)

- **元件裡面又定義元件**（每次 render 都變新型別，state 直接歸零，最嚴重的一條）
- **直接改動 state**：對 `useState` 的值做 `items.push(...)`，畫面永遠不會更新
- `useEffect`：沒有依賴陣列、callback 寫成 async、訂閱沒有 cleanup、effect 太肥
- 用 `.map()` 的 **index 當 `key`**
- hook 太多、props 太多、元件太大、JSX 巢狀太深、JSX 裡塞巢狀三元運算
- Next.js 專案用原生 `<img>`（該用 `next/image`）
- 一般 TypeScript：明寫 `any`、空的 `catch`、`console.log`、檔案過長

嚴重度刻意分級：**會炸的是 warning、結構問題是 information、品味問題是最淡的 hint**，真正重要的訊號才不會被淹掉。

### 3-10 一個 15 行的元件，五條壞味道

下面這個檔案是外掛自己的測試素材，也是第五節儀表板截圖裡那個 `LiveTicker.tsx`。它編得過、跑得起來、看起來人畜無害，五條 warning 全中：

**LiveTicker.tsx（5 條 warning）**

```tsx
import { useEffect, useState } from 'react';

export function LiveTicker({ symbols }: { symbols: string[] }) {
  const [quotes, setQuotes] = useState<string[]>([]);

  useEffect(() => {
    setInterval(() => {
      quotes.push(symbols[0]);
    }, 1000);
  });

  const Row = ({ text }: { text: string }) => <li>{text}</li>;

  return <ul>{quotes.map((q) => <Row text={q} />)}</ul>;
}
```

**修好之後（0 條）**

```tsx
import { useEffect, useState } from 'react';

let nextId = 0;

const Row = ({ text }: { text: string }) => <li>{text}</li>;

export function LiveTicker({ symbols }: { symbols: string[] }) {
  const [quotes, setQuotes] = useState<{ id: number; text: string }[]>([]);

  useEffect(() => {
    const timer = setInterval(() => {
      setQuotes((prev) => [...prev, { id: nextId++, text: symbols[0] }]);
    }, 1000);
    return () => clearInterval(timer);
  }, [symbols]);

  return (
    <ul>
      {quotes.map((q) => <Row key={q.id} text={q.text} />)}
    </ul>
  );
}
```

五條分別是：

| 行 | 規則 id | 它為什麼會咬你 |
|---|---|---|
| `useEffect(() => {…})` | `react.effectWithoutDeps` | 沒有依賴陣列，每次 render 都重跑一次 |
| `setInterval(…)` | `react.effectMissingCleanup` | 沒有 cleanup，重新掛載後舊的那顆還在跑（StrictMode 下直接兩顆） |
| `quotes.push(…)` | `react.mutatedState` | 改的是同一個陣列，reference 沒變，React 看不到要重繪什麼 |
| `const Row = …` | `react.componentInComponent` | 每次 render 都是一個新的元件型別，React 會卸載它、丟掉它的 state 與 DOM |
| `.map((q) => <Row …>)` | `react.missingKey` | 沒給 key，React 安靜地退回用 index |

這五條**都是 warning**，因為每一條都會在 runtime 咬人，而不是「風格不夠好」。修法就是右邊那一欄：定時器拿變數接住並在 cleanup 清掉、`push` 換成產生新陣列的 `setQuotes`、`Row` 搬到模組層、key 綁在資料本身的穩定 id 上。

## 四、C#

![C# refactor 選單，七個 Refactory 動作](https://blog.markkulab.net/content/markku/posts/vscode-refactory-code-health/images/04-csharp-refactor-menu-v2.png)

| 動作 | 它做什麼 |
|---|---|
| **加注入依賴** | 一次改好六個地方（下面詳講） |
| **Extract Interface** | 抽出介面，**放到你專案實際擺介面的資料夾** |
| **介面成員同步** | 介面加了新成員，一鍵在所有實作補上 |
| **DI 註冊** | 把 service 註冊進 DI container |
| **`ConfigureAwait` 補齊** | 整檔補上 `ConfigureAwait(false)` |
| **產生 XML 文件** | 幫成員產生 XML 註解（介面有寫就沿用它的措辭） |
| **控制器 clone 到下一版** | v1 controller 複製成 v2，該改的引用一起改 |

這七個**只在按 `Ctrl+Alt+Shift+T` 時出現**，不會主動跳燈泡（判斷游標在不在 class 裡要問 Roslyn，每次移動都問太慢）。

### 4-1 加注入依賴：一個動作改六個地方

依賴注入（DI）就是「不要自己 `new`，改成叫別人送進來」。手動加一個依賴要改六個地方，漏一個就編不過。

![C# 加注入依賴實機錄影：六筆編輯一次落地](https://blog.markkulab.net/content/markku/posts/vscode-refactory-code-health/images/07-demo-csharp-inject.gif)

> **怎麼操作**：游標放在 class 內**任何位置** → `Ctrl+Alt+Shift+T` → `Add injected dependency…` → 輸入型別（例如 `IPlanService`）→ 輸入命名空間（可留空）

```csharp
// 原本的 class
public class CouponService : ICouponService
{
    /// <summary>優惠券用戶端。</summary>
    private readonly ICouponClient couponClient;

    /// <param name="couponClient">優惠券用戶端。</param>
    public CouponService(ICouponClient couponClient)
    {
        this.couponClient = couponClient;
    }
}
```

**按下去之後，這六個地方同時改好**：

1. `using` 敘述（依字母序插入，不是接在最後面）
2. 新的 `private readonly` 欄位
3. 欄位的 XML 註解
4. 建構子參數
5. `<param>` 標籤（插在跟參數位置對應處，StyleCop 會檢查順序）
6. 建構子裡的 `this.` 賦值

六筆編輯、**一次 undo 全部復原**，落地前先讓你看過：

![Refactor Preview 面板逐條列出這次要落地的編輯，每一條都可以單獨取消](https://blog.markkulab.net/content/markku/posts/vscode-refactory-code-health/images/05-csharp-inject-preview-v2.png)

面板上是五列不是六列——欄位和它上面那行 XML 註解是同一段插入，所以併在同一列顯示。由上到下對應的就是 `using`、欄位（含註解）、`<param>`、建構子參數、`this.` 賦值。

**產出的風格是從你正在編輯的那個 class「量」出來的，不是讀設定檔**。欄位有沒有底線前綴、賦值有沒有 `this.`、有沒有 XML 註解，照現有的樣子生。

兩種情況它會**直接拒絕**：這個 class 有**手動 `new Foo(...)`** 的呼叫點（硬塞 `null!` 編得過但 runtime 會炸）；還有「DI 註冊了沒」它**從不假裝檢查過**，會明說沒驗證並強制走預覽。

### 4-2 Extract Interface：目標資料夾跟鄰居學

> **怎麼操作**：游標放在 class 名稱上 → `Ctrl+Alt+Shift+T` → `Extract Interface`

內建版本會把新檔寫在 class 旁邊，但很多分層專案的介面固定住在鏡像資料夾：

```
BLL/
├── Contracts/Services/Coupons/ICouponService.cs   ← 介面在這
└── Services/Coupons/CouponService.cs              ← 實作在這
```

Refactory 的做法是：**看同資料夾其他已經有介面的 class，查它們的介面住在哪，取多數決**。推導結果會寫在提示裡（例如「依 3 個兄弟類別推得」），學不到也直說。產出的介面檔會帶上你專案的 copyright header、XML 註解與 BOM。

### 4-3 整檔補上 `ConfigureAwait(false)`

函式庫程式碼漏掉 `ConfigureAwait(false)`，在舊版同步環境裡是經典的 deadlock 來源。這個動作**一次掃完整個檔案**，只補漏掉的那幾個。

> **怎麼操作**：游標放在 class 內任何位置 → `Ctrl+Alt+Shift+T` → `Add missing ConfigureAwait(false) in this file`

**補之前**

```csharp
public async Task RunAsync()
{
    var a = await this.client.GetAsync().ConfigureAwait(false);
    var b = await this.client.PostAsync();
    await this.client.FlushAsync();
}
```

**補之後**

```csharp
public async Task RunAsync()
{
    var a = await this.client.GetAsync().ConfigureAwait(false);
    var b = await this.client.PostAsync().ConfigureAwait(false);
    await this.client.FlushAsync().ConfigureAwait(false);
}
```

第一行本來就有，所以**不會被加第二次**。`await Task.Yield()` 會被跳過（它根本沒有 `ConfigureAwait`）。全部補完之後再按一次，它會直接說沒事可做，而不是產生一份空的編輯。

### 4-4 幫成員產生 XML 文件

> **怎麼操作**：游標放在方法 / 屬性 / 建構子的宣告上 → `Ctrl+Alt+Shift+T` → `Document this member`

**沒有註解**

```csharp
public Task<int> GetAsync(string code, int page)
{
    return Task.FromResult(0);
}
```

**產生的骨架**

```csharp
/// <summary></summary>
/// <param name="code"></param>
/// <param name="page"></param>
/// <returns></returns>
public Task<int> GetAsync(string code, int page)
{
    return Task.FromResult(0);
}
```

參數有幾個就有幾個 `<param>`，順序跟著簽章走；`void` 不會生 `<returns>`；建構子直接套 `.refactory.json` 裡設定的那句話（預設是 `Initializes a new instance of the <see cref="Service"/> class.`）。

**這個成員實作了某個介面、而介面那邊已經寫過註解**的話，它會把介面的措辭抄過來——連中文都照抄——並附一條提醒：StyleCop 的 SA1625 討厭一字不差的重複文件，你可能想改寫一下。已經有註解的成員它會拒絕，不會覆蓋你寫過的東西。

### 4-5 C# 壞味道檢查（開檔與存檔時跑）

每條一樣附 `Ctrl+.` 快速修復：

- **Sync-over-async**：在 Task 上用 `.Result` / `.Wait()` / `.GetAwaiter().GetResult()`，經典 deadlock 來源
- async 程式裡出現 `Thread.Sleep`、事件處理器以外的 `async void`
- 建構子注入太多（超過 5 個）、空的 `catch`、catch 了沒 rethrow
- 方法太長、class 太大、檔案太長、巢狀太深
- magic number、巢狀三元運算、public 可變欄位
- 一個檔案塞好幾個頂層型別、TODO / FIXME 標記

門檻是在真實專案上校準過的（85 檔的 Next.js 後台、1,312 檔的分層 C# solution）。`OnModelCreating`、`ConfigureServices` 這種本來就長的方法已豁免長方法規則。

## 五、Code Health 儀表板

> **怎麼操作**：`Ctrl+Shift+P` → **Refactory: Code Health Dashboard**，或點狀態列右下角的 `$(pulse)` 計數器

![剛打開的 Code Health 儀表板：目前這個檔案的五條問題，依檔案分組、每條都寫清楚原因與規則 id](https://blog.markkulab.net/content/markku/posts/vscode-refactory-code-health/images/08-code-health-dashboard.png)

**剛打開時看到的是「目前這個檔案」**（上圖就是還沒掃描的狀態：四張數字卡、沒有 Copy report）。按右上角 **Scan workspace** 才會擴到整個工作區，也才會多出平均健康分數、Trends 與 Hotspots——下面 5-3 那張圖就是掃完之後的樣子。

裡面有三個名詞，白話講一次。

### 5-1 圈複雜度：這個函式有幾條路可以走

數字越大，你要看懂它、或要寫測試蓋滿它，要顧的分支越多。算法是**從 1 開始，每遇到一個岔路就 +1**（`if`、迴圈、每個 `case`、`catch`、三元運算子、每個 `&&` `||` `??`）：

```ts
function getDiscount(user, cart) {          // 起始 1
  if (!user) return 0                       // +1 → 2
  if (user.isVip && cart.total > 1000) {    // +1 (if) +1 (&&) → 4
    return 0.2
  }
  for (const item of cart.items) {          // +1 → 5
    if (item.onSale) return 0.1             // +1 → 6
  }
  return user.coupon ? 0.05 : 0             // +1 → 7
}
```

這個函式是 **7**，預設門檻 **15**，所以很健康。

補充：**匿名 callback 和 lambda 算進最近的具名函式**，所以 React 元件的複雜度會包含裡面的 inline handler 和 `.map()` body（讀這個元件時本來就要一起讀懂）。

### 5-2 健康分數：100 分起跳往下扣

| 扣分項目 | 扣多少 |
|---|---|
| 每個 warning（會炸的問題） | 10 分 |
| 每個 suggestion（結構問題） | 3 分 |
| 每個 hint（品味問題） | 1 分 |
| 函式複雜度超過門檻的部分 | 超出多少扣多少 |

假設某檔案有 2 warning、3 suggestion、1 hint，加一個複雜度 20 的函式（門檻 15，超出 5）：

```
扣分 = 2×10 + 3×3 + 1×1 + 5 = 35
分數 = 100 − 35 = 65 → D
```

等第：**90 以上 A、80–89 B、70–79 C、60–69 D、60 以下 F**。

**分數不會擋 build 也不會跳警告**，它只用來排序，回答「我今天該先看哪個檔案」。

### 5-3 Hotspots：常改 × 不健康 = 最該先修

一個很爛但半年沒人動的檔案，跟一個天天被改的檔案，優先順序完全不同。所以把兩件事**相乘**：

```
熱點分數 = 最近常不常改 × 有多不健康（100 − 健康分數）
```

**關鍵是相乘不是相加**，任一邊是 0 結果就是 0：很爛但沒人動的、天天改但很乾淨的，都不會排前面；**又常改又不健康的才會排到最上面**。

「常不常改」是跑一次 `git log`，數這個檔案最近 90 天被幾個 commit 動過。沒有 git 的話這區塊直接不出現，不會跳錯誤煩你。

![Metrics、Trends 與 Hotspots：趨勢長條圖、熱點排名與最複雜的函式清單](https://blog.markkulab.net/content/markku/posts/vscode-refactory-code-health/images/09-metrics-trends-hotspots.png)

### 5-4 面板怎麼用

掃過一次之後，由上到下是：**五張數字卡**（smells / warnings / suggestions / hints，加上掃描後才出現的平均健康分數）→ **Trends**（每掃一次記一根長條，最近 30 次，看得出這週比上週好還是壞）→ **Hotspots** → **Most complex functions**（最複雜的 10 個，點了就跳過去）→ **檔案清單**（等第徽章 + 逐條問題，點一下跳到那一行）。

| 按鈕 | 做什麼 |
|---|---|
| **Scan workspace** | 掃整個工作區。直接讀硬碟，不開檔也不灌爆 Problems 面板，可中途取消。TS/JS 與 C# 各自最多 2,000 檔 |
| **Copy report** | 把發現轉成 Markdown 複製，直接貼進 PR 描述（掃過一次才會出現） |
| **Refresh** | 重新整理 |
| 右上角那排色點 | 八種面板主題，第一個是跟著 VS Code 佈景走 |

### 5-5 一鍵清掉機械性的問題

> **怎麼操作**：`Ctrl+Shift+P` → **Refactory: Clean Up This File**

- TypeScript / JavaScript：刪掉所有「獨佔一行」的 `console.log(...)`
- C#：把 catch 裡的 `throw ex;` 改成 `throw;`（保住 stack trace）

跑完會回報做了什麼（例如 `3 console.logs removed, 1 rethrow fixed`），**一個 undo 全部復原**。

## 六、一鍵交給本機 Claude Code 修

本機有裝 [Claude Code](https://claude.com/claude-code) CLI 且在 PATH 上的話，會多出三個入口：

> **入口一**：任一條壞味道的 `Ctrl+.` 選單多一項 **`Fix with Claude Code…`**
> **入口二**：儀表板每個檔案右側的 **`✦ Review`**（Hotspots 區塊還有 `✦ Review top 3`）
> **入口三**：`Ctrl+Shift+P` → **Refactory: Deep Review This File with Claude (AI)**，不用先開儀表板

**入口一**會開整合終端機，跑**你自己的** `claude` session，prompt 帶上規則 id、檔案、行號與訊息，並要求改動最小化。

**入口二與入口三**會先寫一份 briefing 給 Claude（行數、函式數、最高與平均複雜度、健康分數與等第、最複雜的幾個函式、最近哪幾個 commit 動過它、所有找到的問題），請它產出**排好優先順序**的重構建議。briefing 寫在 `.refactory/reviews/`，那個資料夾自己帶一個內容為 `*` 的 `.gitignore`，不會被 commit。

你的登入、你的模型、你的核可流程，外掛只負責把 prompt 寫好。**沒有任何東西會被自動套用**，你看著 diff 落地。

## 七、另外 13 種語言：最常用的那幾個重構

Python、Go、Java、Kotlin、Rust、PHP、C、C++、Objective-C、Objective-C++、Dart、Swift、Scala 各有 11 個結構性重構，**不用 language server、不用 SDK、什麼都不用裝**。開一個 `.go` 檔，就算你連 Go 擴充都沒裝，這些照樣出得來。

| 動作 | 在哪些語言 |
|---|---|
| Invert if condition | 全部 |
| Merge nested if · Split if condition | 全部 |
| Add braces · Remove braces | 有大括號的那幾種 |
| Introduce variable · Inline variable | 全部（TS / JS 除外，編輯器內建的更好用） |
| Apply De Morgan's law | 全部 |
| Replace if with `?:` | 除了 Go（它沒有三元運算子） |
| 轉插值字串 | Python、C#、Kotlin、PHP、Dart、Swift |
| `.format()` / `%` 轉 f-string | Python |

> **怎麼操作**：游標放在 `if` 關鍵字或它的**條件式**上（不是 body 裡隨便哪裡）→ `Ctrl+Alt+Shift+T`

游標一定要在條件式上，否則四層巢狀的 `if` 會同時全部提供，你分不出哪個是哪個。

**注意**：除了 Introduce variable 和轉插值字串（這兩個讓給 VS Code 內建的型別感知版本），上面這些在 **TypeScript / JavaScript 一樣用得到**。下面的例子挑不同語言寫，只是為了展示同一個動作在各語言會產生該語言的寫法。

### 7-1 Invert if：把 happy path 拉回左邊

> 游標放在 `if` 上 → `Invert if condition`

**反轉前**

```go
if err == nil {
    process(data)
} else {
    return err
}
```

**反轉後**

```go
if err != nil {
    return err
} else {
    process(data)
}
```

它做的是**條件取反 + 兩個分支對調**，不會自作主張幫你把 `else` 拆掉——那是另一個決定，不該混在同一個動作裡。條件本來就有 `!` 的話會**把它拿掉**，而不是疊成 `!!`；`!(a && b)` 這種會連括號一起收乾淨變成 `a && b`。

一個別人不會提的小地方：`if (a < b)` 反轉成 `if (a >= b)`，**在浮點數上這兩者不等價**（遇到 NaN 兩邊都是 false）。運算元看起來像浮點數時它會跳提醒並強制走預覽，市面上的工具多半直接翻給你。

### 7-2 Merge nested if：把金字塔壓平

> 游標放在**外層**的 `if` 上 → `Merge nested if`

**兩層守衛**

```ts
function f(user) {
  if (user) {
    if (user.isActive) {
      send(user)
      log(user)
    }
  }
}
```

**合併後**

```ts
function f(user) {
  if (user && user.isActive) {
    send(user)
    log(user)
  }
}
```

Python 也吃，而且會**連縮排一起收回來**（用的是 `and`，不是 `&&`）：

**兩層守衛**

```python
def f(user):
    if user:
        if user.is_active:
            send(user)
            log(user)
```

**合併後**

```python
def f(user):
    if user and user.is_active:
        send(user)
        log(user)
```

反過來的 **Split if condition** 是同一個選單裡的另一項：把 `if (a && b)` 拆回兩層，通常是因為你要在中間那層插一個 `else`。

### 7-3 De Morgan：`!(a && b)` 兩個方向都能走

**套用前**

```ts
if (!(a && b)) {
  x()
}
```

**套用後**

```ts
if (!a || !b) {
  x()
}
```

**兩個方向都提供**，`if (!a || !b)` 上按下去會收回成 `if (!(a && b))`，來回轉會拿到一模一樣的原文。比較運算子不會被硬加否定，而是直接翻面：

**套用前**

```ts
if (!(a === 1 && b > 2)) {
  x()
}
```

**套用後**

```ts
if (a !== 1 || b <= 2) {
  x()
}
```

Python 用的是它自己的字：`if not (ready and done):` ⟷ `if not ready or not done:`。已經是 `if (a && b)` 這種沒有否定的條件不會提供這個動作——沒有東西可以分配。

### 7-4 if 轉條件運算式：每種語言寫法不一樣

同一個動作，產出的是**那個語言真正的寫法**，不是硬套 `?:`：

**Java：兩個賦值**

```java
if (user.isAdmin()) {
    role = "admin";
} else {
    role = "guest";
}
```

**Java：三元運算子**

```java
role = user.isAdmin() ? "admin" : "guest";
```

**Python：兩個 return**

```python
def role(user):
    if user.is_admin:
        return "admin"
    else:
        return "guest"
```

**Python：條件表達式**

```python
def role(user):
    return "admin" if user.is_admin else "guest"
```

Kotlin 拿到的是 if-expression（`role = if (user.isAdmin) "admin" else "guest"`），Rust 是大括號版的 if-expression（`role = if user.is_admin { "admin" } else { "guest" };`）。兩個分支必須是**同一件事**——都是 return，或都是賦值給同一個東西——否則不提供。

### 7-5 Python：`.format()` / `%` 轉 f-string

> 游標放在字串字面值裡 → `Convert to an f-string`

**format()**

```python
msg = "Hello, {}!".format(name)
row = "{:>8}|{:<8}".format(left, right)
title = "Hi {who}".format(who=user.name)
```

**f-string**

```python
msg = f"Hello, {name}!"
row = f"{left:>8}|{right:<8}"
title = f"Hi {user.name}"
```

對齊格式（`:>8`）與轉換旗標（`!r`）原封不動搬過去，`{1} … {0}` 這種明寫位置的也會照著對回正確的引數。

**`"%d" % value` 刻意不轉**：`"%d" % 3.7` 的結果是 `"3"`，`f"{3.7}"` 是 `"3.7"`——這不是格式差異，是值變了。只有 `%s` 和 `%r` 轉過去意思完全不變，所以只有這兩個會轉。

## 八、設定

| 設定 | 預設 | 說明 |
|---|---|---|
| `refactory.preview` | `multiFile` | 什麼時候走預覽面板。**帶警告的計畫一律強制預覽** |
| `refactory.dotnetPath` | `""` | 指定 C# 引擎用的 `dotnet` 路徑 |
| `refactory.disabledLanguages` | `[]` | 要完全避開的語言 id |
| `refactory.smells.react` / `.csharp` | 開 | 各自關掉一半的壞味道檢查 |
| `refactory.developerMode` | `false` | 開啟 `Dev:` 診斷指令 |
| `refactory.keymap` | `none` | 設 `riderStyle` 會多綁 `Ctrl+Alt+M` / `Ctrl+Alt+V` 到 VS Code **內建**的抽取動作。預設關掉，因為非美式鍵盤上 `Ctrl+Alt+<字母>` 跟 `AltGr+<字母>` 分不出來 |

### 教它你的專案慣例：`.refactory.json`

慣例是**專案的屬性**，不是個人偏好，所以放 repo 根目錄、進版控、全團隊共用。**所有欄位都可省略，沒寫的就用自動偵測的**：

```jsonc
{
  "version": 1,
  "typescript": {
    "components": { "declaration": "exportDefaultFunction", "propsStyle": "interfaceSuffixProps" },
    "hooks": {
      // 先寫特定路徑、最後放 catch-all，第一個符合的贏
      "location": [
        { "when": "src/components/admin/**", "dir": "src/components/admin/_hooks" },
        { "when": "**", "dir": "src/hooks" }
      ],
      "fileNaming": "camelCase"
    }
  },
  "smells": { /* 個別規則關閉或調門檻，id 見 docs/SMELLS.md */ },
  "exclude": ["**/node_modules/**", "**/.next/**"]
}
```

`exclude` 同時也管壞味道檢查，所以打開 generated 的 migration 檔不會滿屏波浪線。

## 九、按了卻「什麼都沒出現」

> **怎麼操作**：設定 `"refactory.developerMode": true` → `Ctrl+Shift+P` → **Refactory: Dev: Why Is Nothing Offered Here?**

它會直接告訴你：哪份 tsconfig 管這個檔、推導出哪些 alias、語言認不認得、檔案能不能正常解析、游標是不是在字串或註解裡、哪些動作是刻意讓給編輯器內建的。

**最常見的元兇是前面有一個沒閉合的字串**，會讓後面整個檔案失效。還是想不通，**Refactory** 這個 output channel 有完整 trace。

## 注意事項

- **繁體中文介面**：VS Code 語言設繁中就會看到「重構這裡…」「程式碼健康儀表板」
- **C#** 需要 .NET 8 runtime；純前端的工作階段根本不會啟動 .NET sidecar
- **Kotlin** 要先裝任一個 Kotlin 外掛（VS Code 自己沒註冊 `kotlin` 語言 id）
- **Ruby 刻意不支援**：詞法結構沒有 parser 就是真的有歧義，一張百分之一機率出錯的分析表比不支援更糟
- TypeScript / JavaScript 上刻意**不提供** Introduce variable 和轉插值字串，內建版本懂型別，比較好用
- **不會拖慢 VS Code**：不載入 solution、不載入 TypeScript program，跨檔問題用宣告索引回答（上千檔一秒內建完）

## 結論

通用的重構各家原廠都有做。但**知道你的專案把介面放哪、DI 註冊寫在哪、`'use client'` 該不該加**，這件事只有懂你 repo 的工具做得到。

- 安裝：[VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=mark-ku.refactory) 搜 **Refactory**，或 `ext install mark-ku.refactory`
- 工具頁：[Refactory 介紹頁](https://blog.markkulab.net/tools/refactory)（功能總覽、截圖與介紹影片）

裝起來按一下 `Ctrl+Alt+Shift+T`，你就知道少的那一味是什麼了。

---

## 關於本文與作者

本文出自 [Mark Ku's Blog](https://blog.markkulab.net/post/vscode-refactory-code-health)

授權條款： [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — 轉載或引用請註明作者並附上原文連結

### 關於作者

**[Mark Ku](https://blog.markkulab.net/author/mark-ku)** — Software Solution Provider

- 10+ 年資深軟體工程師，現為 AI 應用 Builder
- 專注大型平台架構設計，從北美電商到AI SaaS訂閱收費系統
- 結合 AI Agent 與自動化，打造高效可演進的產品技術基礎

### 作者開發的免費工具

以下工具皆可免費使用：

- [免費 PDF 簽名工具](https://blog.markkulab.net/tools/pdf-sign): 線上 PDF 簽名工具，瀏覽器內完成手繪、打字、上傳簽名，可拖曳放置、縮放、下載。所有處理都在你的裝置完成，檔案不會上傳。
- [VS Code Refactory](https://blog.markkulab.net/tools/refactory): Refactory 是一款 VS Code 重構擴充套件：34 個重構動作、37 條 code smell 檢查、Code Health 儀表板、18 種語言、534 支測試。懂你的專案慣例：介面放哪、DI 註冊寫在哪、'use client' 該不該加；還會用 git 修改頻率 × 複雜度排出「該先修哪個檔案」，並一鍵把壞味道交給你自己電腦上的 Claude Code 修。免費使用，原始碼不離開你的機器。
- [DB-Kit 資料庫管理工具](https://blog.markkulab.net/tools/db-kit): DB-Kit 是一個用 Tauri + Rust + React 打造的輕量跨平台資料庫管理工具，用單一一致的介面同時管理 MySQL、MariaDB、PostgreSQL、SQL Server、Oracle、SQLite、MongoDB、Redis、Kafka、Elasticsearch 與 RabbitMQ 十一種資料來源：連線密碼以 OS keychain 加密、SSH Tunnel、完整 CRUD、視覺化查詢建構器、多結果集同時顯示、跨連線資料傳輸與比對同步、Excel / CSV 匯入匯出、執行計畫視覺化、ER 圖、排程備份、SQL 壓力測試（p50～p99 延遲百分位）、15 條規則的 SQL 審查、Kafka 訊息瀏覽與監控告警；繁中 / 英文雙語介面，內建 AI 助手（自然語言生成 SQL、AI 審查與調校建議）與命令列工具 dbk。免費開源（MIT），提供 Windows / macOS / Linux 安裝檔。
- [VS Code Super Mermaid](https://blog.markkulab.net/tools/super-mermaid): Super Mermaid 是一款 VS Code 擴充套件：開箱即用的漂亮 Mermaid 圖表，自動上色、即時預覽、滑鼠平移縮放、PNG / SVG 高解析匯出，內建 21 種範本與多種主題。免費開源（MIT）。
- [React Super Mermaid](https://blog.markkulab.net/tools/react-super-mermaid): react-super-mermaid 是一個開源 React 元件庫：一行 <MermaidViewer> 即可渲染漂亮的 Mermaid 圖表，內建 colorful / sketch 主題、平移縮放、圖內搜尋、SVG / PNG 高解析匯出。輕量、SSR 安全、完整 TypeScript 型別。免費開源（MIT）。
- [Jira / Confluence Super Mermaid](https://blog.markkulab.net/tools/jira-super-mermaid): Atlassian Forge app：在 Jira issue 與 Confluence 內文直接寫 Mermaid 語法，畫流程圖、時序圖、狀態機與甘特圖。11 種圖表、SVG / PNG 匯出、明暗主題、完整中日韓文字支援。取得 Runs on Atlassian 資格：圖表存在你自己的站台，app 不呼叫任何第三方服務。免費，即將上架 Atlassian Marketplace。
- [Mermaid 線上預覽](https://blog.markkulab.net/tools/mermaid-preview): 在瀏覽器裡寫 Mermaid、即時看圖，整張圖表壓進網址就能分享。免註冊、不上傳伺服器，相容 mermaid.live 的分享連結。
- [React Intl Phone Number](https://blog.markkulab.net/tools/react-intl-phone-number): react-intl-phone-number 是一個開源 React 元件：framework-agnostic、不依賴 antd，提供 E.164 進出、可搜尋國旗 / 國碼下拉、可配置驗證等級（strict / mobile-strict / loose）、可主題化 CSS 與 i18n，電話邏輯由 google-libphonenumber 驅動。輕量、完整 TypeScript 型別。免費開源（MIT）。
- [Uptime Kuma Cluster](https://blog.markkulab.net/tools/uptime-kuma-cluster): 把單機版 Uptime Kuma 改造成高可用叢集：OpenResty + Lua 智慧負載平衡、MariaDB 共享狀態、健康檢查與自動 Failover，附叢集管理 REST API，一行 Docker Compose 啟動。免費開源（MIT）。
- [特教專案](https://blog.markkulab.net/education): 為特殊教育學生製作的學習教材

### 每日 Podcast

- [科技新鮮事](https://blog.markkulab.net/category/tech-news): 每日精選 AI 與科技趨勢，透過語音摘要快速掌握最新技術動態，涵蓋 AI 應用、軟體架構、DevOps 與工程實戰。 — RSS: https://blog.markkulab.net/feed.xml
- [AI股市蝦聊](https://blog.markkulab.net/category/ai-stock-chat): 每個交易日用 AI 分析台股盤勢，以雙人對話聊當天的盤中觀察與隔日預測。 — RSS: https://blog.markkulab.net/ai-stock-chat/feed.xml

### 電子報

[訂閱電子報](https://blog.markkulab.net/subscribe) — 第一時間收到新文章通知，無垃圾信、隨時可取消訂閱。
