Add implementation plan for pre-submit code auto-formatting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 07:43:51 -06:00
parent 1bbc149e55
commit 69eb15ac27

View File

@@ -0,0 +1,698 @@
# 提交前自动代码格式化 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 用户提交 Python3/C/C++ 代码时,前端先调用新增的 `/api/format_code` 接口(后端用 `ruff format` / `clang-format`)格式化代码,更新编辑器内容后再提交;其他语言原样提交,格式化失败时按语言区分降级。
**Architecture:** 后端在 `submission` 应用新增一个同步 `APIView``FormatCodeAPI`),内部通过 `subprocess` 调用 `ruff format``clang-format`,返回格式化后的代码或错误码(`format-error` / `server-error`)。前端在 `SubmitCode.vue``submit()` 中,对 Python3/C/C++ 调用该接口,成功则写回 `codeStore`(驱动编辑器与 Yjs 同步),`format-error` 阻止提交并提示,其他错误静默降级提交原代码。
**Tech Stack:** Django 6 + DRF (`utils.api.APIView`), `subprocess`, `ruff` CLI, `clang-format` CLIVue 3 + Pinia + axios (`utils/http.ts`)
> 注:本项目 `CLAUDE.md` 明确"不写测试"。本计划用手动验证dev server + curl / 浏览器)代替自动化测试步骤。
---
## Backend Tasks
### Task 1: 新建 `submission/utils.py` — 格式化工具封装
**Files:**
- Create: `OnlineJudge/submission/utils.py`
- [ ] **Step 1: 编写格式化函数与异常类**
```python
import subprocess
CLANG_FORMAT_STYLE = "{BasedOnStyle: LLVM, IndentWidth: 4, BreakBeforeBraces: Attach}"
class FormatSyntaxError(Exception):
"""格式化工具因代码本身存在语法错误而失败(目前只有 ruff format 会触发)"""
class FormatToolError(Exception):
"""格式化工具执行本身失败:超时、二进制缺失、非预期的非零退出码"""
def format_code(code, language):
"""
:param code: 用户代码
:param language: "python" | "c" | "cpp"
:return: 格式化后的代码字符串
:raises FormatSyntaxError: 代码语法错误导致格式化失败(仅 python
:raises FormatToolError: 格式化工具本身执行失败
"""
if language == "python":
return _format_with_ruff(code)
return _format_with_clang(code, language)
def _format_with_ruff(code):
try:
result = subprocess.run(
["ruff", "format", "--stdin-filename", "code.py", "-"],
input=code,
capture_output=True,
text=True,
timeout=5,
)
except (subprocess.TimeoutExpired, OSError) as e:
raise FormatToolError(str(e))
if result.returncode != 0:
raise FormatSyntaxError(result.stderr)
return result.stdout
def _format_with_clang(code, language):
filename = "code.c" if language == "c" else "code.cpp"
try:
result = subprocess.run(
[
"clang-format",
f"-assume-filename={filename}",
f"-style={CLANG_FORMAT_STYLE}",
],
input=code,
capture_output=True,
text=True,
timeout=5,
)
except (subprocess.TimeoutExpired, OSError) as e:
raise FormatToolError(str(e))
if result.returncode != 0:
raise FormatToolError(result.stderr)
return result.stdout
```
- [ ] **Step 2: 手动验证 ruff 分支**
Run:
```bash
cd OnlineJudge
uv run python -c "
from submission.utils import format_code
print(repr(format_code('x=1\ndef f( a,b ):\n return a+b\n', 'python')))
"
```
Expected: 输出格式化后的代码4 空格缩进、`x = 1`、空两行分隔函数等),不抛异常。
- [ ] **Step 3: 手动验证 ruff 语法错误分支**
Run:
```bash
uv run python -c "
from submission.utils import format_code, FormatSyntaxError
try:
format_code('def f(:\n pass\n', 'python')
except FormatSyntaxError as e:
print('FormatSyntaxError:', e)
"
```
Expected: 打印 `FormatSyntaxError: ...` 且包含 ruff 的语法错误信息。
- [ ] **Step 4: 手动验证 clang-format 分支**
Run:
```bash
uv run python -c "
from submission.utils import format_code
print(format_code('#include<stdio.h>\nint main(){\nint a=1;\nreturn 0;}\n', 'cpp'))
"
```
Expected: 输出 4 空格缩进、`{` 跟在行尾Attach 风格)的格式化代码。
- [ ] **Step 5: Commit**
```bash
git add submission/utils.py
git commit -m "feat(submission): add ruff/clang-format code formatting helper"
```
---
### Task 2: 新增 `FormatCodeSerializer`
**Files:**
- Modify: `OnlineJudge/submission/serializers.py`
- [ ] **Step 1: 在文件末尾追加序列化器**
`submission/serializers.py` 末尾追加:
```python
class FormatCodeSerializer(serializers.Serializer):
code = serializers.CharField(max_length=1024 * 1024)
language = serializers.ChoiceField(choices=("python", "c", "cpp"))
```
- [ ] **Step 2: 手动验证**
Run:
```bash
cd OnlineJudge
uv run python -c "
from submission.serializers import FormatCodeSerializer
s = FormatCodeSerializer(data={'code': 'x=1', 'language': 'python'})
print(s.is_valid(), s.validated_data)
s2 = FormatCodeSerializer(data={'code': 'x=1', 'language': 'java'})
print(s2.is_valid(), s2.errors)
"
```
Expected: 第一行 `True {'code': 'x=1', 'language': 'python'}`;第二行 `False {'language': [ErrorDetail(string='\"java\" is not a valid choice.', code='invalid_choice')]}`
- [ ] **Step 3: Commit**
```bash
git add submission/serializers.py
git commit -m "feat(submission): add FormatCodeSerializer"
```
---
### Task 3: 新增 `FormatCodeAPI` 视图
**Files:**
- Modify: `OnlineJudge/submission/views/oj.py`
- [ ] **Step 1: 在文件顶部导入区添加 logging 和新依赖**
当前顶部导入(`submission/views/oj.py:1-16`
```python
import ipaddress
from asgiref.sync import sync_to_async
from django.utils import timezone
from account.decorators import check_contest_permission, login_required
from contest.models import ContestStatus
from flowchart.models import FlowchartSubmission
from judge.tasks import judge_task
from options.options import SysOptions
# from judge.dispatcher import JudgeDispatcher
from problem.models import Problem, ProblemRuleType
from utils.api import AsyncAPIView, validate_serializer
from utils.cache import cache
from utils.captcha import Captcha
from utils.throttling import TokenBucket
from ..models import Submission
from ..serializers import (
CreateSubmissionSerializer,
ShareSubmissionSerializer,
SubmissionListSerializer,
SubmissionModelSerializer,
SubmissionSafeModelSerializer,
bulk_fetch_problemset_progress,
)
```
修改为(新增 `import logging``APIView``FormatCodeSerializer``from ..utils import ...`
```python
import ipaddress
import logging
from asgiref.sync import sync_to_async
from django.utils import timezone
from account.decorators import check_contest_permission, login_required
from contest.models import ContestStatus
from flowchart.models import FlowchartSubmission
from judge.tasks import judge_task
from options.options import SysOptions
# from judge.dispatcher import JudgeDispatcher
from problem.models import Problem, ProblemRuleType
from utils.api import APIView, AsyncAPIView, validate_serializer
from utils.cache import cache
from utils.captcha import Captcha
from utils.throttling import TokenBucket
from ..models import Submission
from ..serializers import (
CreateSubmissionSerializer,
FormatCodeSerializer,
ShareSubmissionSerializer,
SubmissionListSerializer,
SubmissionModelSerializer,
SubmissionSafeModelSerializer,
bulk_fetch_problemset_progress,
)
from ..utils import FormatSyntaxError, FormatToolError, format_code
logger = logging.getLogger(__name__)
```
- [ ] **Step 2: 在文件末尾追加视图类**
`submission/views/oj.py` 文件末尾追加:
```python
class FormatCodeAPI(APIView):
@login_required
@validate_serializer(FormatCodeSerializer)
def post(self, request):
code = request.data["code"]
language = request.data["language"]
try:
formatted = format_code(code, language)
except FormatSyntaxError as e:
return self.error(msg=str(e), err="format-error")
except FormatToolError as e:
logger.exception("format_code tool error: %s", e)
return self.error(msg="format failed", err="server-error")
return self.success({"code": formatted})
```
- [ ] **Step 3: 手动验证(需先启动 dev server**
Run另开终端:
```bash
cd OnlineJudge && python dev.py
```
确认服务起来后,用已登录用户的 session/cookie 验证(替换 `<csrftoken>`/`<sessionid>` 为实际 cookie 值,可从浏览器登录后开发者工具中获取):
```bash
curl -s -X POST http://localhost:8000/api/format_code \
-H "Content-Type: application/json" \
-H "Cookie: sessionid=<sessionid>; csrftoken=<csrftoken>" \
-H "X-CSRFToken: <csrftoken>" \
-d '{"code": "x=1\ndef f( a,b ):\n return a+b\n", "language": "python"}'
```
Expected: `{"error": null, "data": {"code": "x = 1\n\n\ndef f(a, b):\n return a + b\n"}}`
未登录时(不带 Cookie请求应返回 `{"error": "login-required", ...}`
- [ ] **Step 4: Commit**
```bash
git add submission/views/oj.py
git commit -m "feat(submission): add FormatCodeAPI view"
```
---
### Task 4: 注册 URL 路由
**Files:**
- Modify: `OnlineJudge/submission/urls/oj.py`
- [ ] **Step 1: 修改文件**
当前内容:
```python
from django.urls import path
from ..views.oj import (
ContestSubmissionListAPI,
SubmissionAPI,
SubmissionExistsAPI,
SubmissionListAPI,
SubmissionsTodayCount,
)
urlpatterns = [
path("submission", SubmissionAPI.as_view()),
path("submissions", SubmissionListAPI.as_view()),
path("submissions/today_count", SubmissionsTodayCount.as_view()),
path("submission_exists", SubmissionExistsAPI.as_view()), # DEPRECATED: 前端未调用
path("contest_submissions", ContestSubmissionListAPI.as_view()),
]
```
改为:
```python
from django.urls import path
from ..views.oj import (
ContestSubmissionListAPI,
FormatCodeAPI,
SubmissionAPI,
SubmissionExistsAPI,
SubmissionListAPI,
SubmissionsTodayCount,
)
urlpatterns = [
path("submission", SubmissionAPI.as_view()),
path("submissions", SubmissionListAPI.as_view()),
path("submissions/today_count", SubmissionsTodayCount.as_view()),
path("submission_exists", SubmissionExistsAPI.as_view()), # DEPRECATED: 前端未调用
path("contest_submissions", ContestSubmissionListAPI.as_view()),
path("format_code", FormatCodeAPI.as_view()),
]
```
- [ ] **Step 2: 手动验证**
Rundev server 已启动的前提下):
```bash
curl -s -X POST http://localhost:8000/api/format_code -H "Content-Type: application/json" -d '{}'
```
Expected: 返回 `{"error": "login-required", ...}`(未登录),说明路由已生效(而不是 404
- [ ] **Step 3: Commit**
```bash
git add submission/urls/oj.py
git commit -m "feat(submission): register /api/format_code route"
```
---
### Task 5: 将 `ruff` 移入主依赖
**Files:**
- Modify: `OnlineJudge/pyproject.toml`
- Modify: `OnlineJudge/deploy/requirements.txt` (通过命令生成,不手写)
- [ ] **Step 1: 编辑 `pyproject.toml`**
当前 `[project] dependencies` 列表末尾(`pyproject.toml`
```toml
"asgiref>=3.11.1",
"jieba>=0.42.1",
]
[dependency-groups]
dev = [
"ruff>=0.15.11",
]
```
改为(`ruff` 移入 `dependencies`,删除 `[dependency-groups]`
```toml
"asgiref>=3.11.1",
"jieba>=0.42.1",
"ruff>=0.15.11",
]
```
> 如果 `[dependency-groups]` 块下还有其他依赖项,只删除 `"ruff>=0.15.11",` 这一行并保留其余内容;本仓库当前该块只有 ruff 一项,可整块删除。
- [ ] **Step 2: 同步 lock 文件与 requirements**
Run:
```bash
cd OnlineJudge
uv sync
./reqs.sh
```
Expected: `uv.lock` 更新ruff 从 dev group 移到主依赖组),`deploy/requirements.txt` 重新生成并包含 `ruff==...` 一行。
- [ ] **Step 3: 验证 ruff 仍可用**
Run:
```bash
uv run ruff --version
```
Expected: 输出版本号(如 `ruff 0.15.12`),不报错。
- [ ] **Step 4: Commit**
```bash
git add pyproject.toml uv.lock deploy/requirements.txt
git commit -m "build: move ruff from dev to main dependencies (needed at runtime for code formatting)"
```
---
### Task 6: Dockerfile 增加 `clang-format`
**Files:**
- Modify: `OnlineJudge/Dockerfile`
- [ ] **Step 1: 修改 `apk add` 列表**
当前(`Dockerfile:17-23`
```dockerfile
apk add --no-cache \
gcc libc-dev python3-dev \
libpq libpq-dev \
libjpeg-turbo libjpeg-turbo-dev \
zlib zlib-dev \
freetype freetype-dev \
supervisor openssl nginx curl unzip
```
改为(新增 `clang-extra-tools`,提供 `clang-format` 二进制;放在常驻依赖里,不会被后面的 `apk del` 清理):
```dockerfile
apk add --no-cache \
gcc libc-dev python3-dev \
libpq libpq-dev \
libjpeg-turbo libjpeg-turbo-dev \
zlib zlib-dev \
freetype freetype-dev \
supervisor openssl nginx curl unzip \
clang-extra-tools
```
- [ ] **Step 2: 验证镜像可构建并包含 clang-format**
Run:
```bash
cd OnlineJudge
docker build -t oj-backend-format-test .
docker run --rm oj-backend-format-test clang-format --version
```
Expected: 构建成功,且输出 `clang-format version ...`(不报 `command not found`)。
> 该步骤构建耗时较长,如本地环境无法运行 docker可跳过实际构建但需在 PR 描述中说明未验证,留待 CI/部署时确认。
- [ ] **Step 3: Commit**
```bash
git add Dockerfile
git commit -m "build: install clang-extra-tools for clang-format in backend image"
```
---
## Frontend Tasks
### Task 7: 新增 `formatCode` API 函数
**Files:**
- Modify: `ojnext/src/oj/api.ts`
- [ ] **Step 1: 在 `submitCode` 函数后追加新函数**
当前(`ojnext/src/oj/api.ts:61-63`
```ts
export function submitCode(data: SubmitCodePayload) {
return http.post("submission", data)
}
```
改为:
```ts
export function submitCode(data: SubmitCodePayload) {
return http.post("submission", data)
}
export function formatCode(data: { code: string; language: string }) {
return http.post<{ code: string }>("format_code", data)
}
```
- [ ] **Step 2: 手动验证**
Run:
```bash
cd ojnext
npx tsc --noEmit
```
Expected: 无新增类型错误(与修改前结果一致)。
- [ ] **Step 3: Commit**
```bash
git add src/oj/api.ts
git commit -m "feat(api): add formatCode endpoint for pre-submit formatting"
```
---
### Task 8: `SubmitCode.vue` 集成提交前自动格式化
**Files:**
- Modify: `ojnext/src/oj/problem/components/SubmitCode.vue`
- [ ] **Step 1: 添加导入**
当前导入区(`SubmitCode.vue:1-14`
```ts
import { Icon } from "@iconify/vue"
import { storeToRefs } from "pinia"
import { getComment, submitCode, updateProblemSetProgress } from "oj/api"
import { useCodeStore } from "oj/store/code"
import { useProblemStore } from "oj/store/problem"
import { useFireworks } from "oj/problem/composables/useFireworks"
import { useSubmissionMonitor } from "oj/problem/composables/useSubmissionMonitor"
import { SubmissionStatus } from "utils/constants"
import type { SubmitCodePayload } from "utils/types"
import SubmissionResult from "./SubmissionResult.vue"
import { useBreakpoints } from "shared/composables/breakpoints"
import { useUserStore } from "shared/store/user"
import { checkPythonSyntax } from "oj/problem/utils/pythonSyntaxCheck"
```
改为(新增 `formatCode` 导入、`LANGUAGE_FORMAT_VALUE` 导入):
```ts
import { Icon } from "@iconify/vue"
import { storeToRefs } from "pinia"
import {
formatCode,
getComment,
submitCode,
updateProblemSetProgress,
} from "oj/api"
import { useCodeStore } from "oj/store/code"
import { useProblemStore } from "oj/store/problem"
import { useFireworks } from "oj/problem/composables/useFireworks"
import { useSubmissionMonitor } from "oj/problem/composables/useSubmissionMonitor"
import { LANGUAGE_FORMAT_VALUE, SubmissionStatus } from "utils/constants"
import type { SubmitCodePayload } from "utils/types"
import SubmissionResult from "./SubmissionResult.vue"
import { useBreakpoints } from "shared/composables/breakpoints"
import { useUserStore } from "shared/store/user"
import { checkPythonSyntax } from "oj/problem/utils/pythonSyntaxCheck"
```
- [ ] **Step 2: 在 `submit()` 中插入格式化步骤**
当前 `submit()` 函数(`SubmitCode.vue:110-139`
```ts
async function submit() {
if (!userStore.isAuthed) return
// 0. Python3 语法检测
if (codeStore.code.language === "Python3") {
const syntaxError = checkPythonSyntax(codeStore.code.value)
if (syntaxError) {
message.warning(`${syntaxError.line} 行存在语法错误,请修正后再提交`)
return
}
}
// 1. 构建提交数据
const data: SubmitCodePayload = {
problem_id: problem.value!.id,
language: codeStore.code.language,
code: codeStore.code.value,
}
if (contestID) {
data.contest_id = parseInt(contestID)
}
// 2. 提交代码到后端
const res = await submitCode(data)
console.log(`[Submit] 代码已提交: ID=${res.data.submission_id}`)
// 3. 启动冷却 + 监控
startCooldown()
startMonitoring(res.data.submission_id)
showResult.value = true
}
```
改为(在"1. 构建提交数据"之前插入"0.5 提交前自动格式化"
```ts
async function submit() {
if (!userStore.isAuthed) return
// 0. Python3 语法检测
if (codeStore.code.language === "Python3") {
const syntaxError = checkPythonSyntax(codeStore.code.value)
if (syntaxError) {
message.warning(`${syntaxError.line} 行存在语法错误,请修正后再提交`)
return
}
}
// 0.5 提交前自动格式化Python3 用 ruffC/C++ 用 clang-format
const formatLang = LANGUAGE_FORMAT_VALUE[codeStore.code.language]
if (["python", "c", "cpp"].includes(formatLang)) {
try {
const res = await formatCode({
code: codeStore.code.value,
language: formatLang,
})
codeStore.setCode(res.data.code)
} catch (e: any) {
if (e?.error === "format-error") {
// 仅 Python3 会出现:代码本身存在语法错误
message.warning(`代码格式化失败:${e.data},请检查代码后重试`)
return
}
// server-error / 网络异常:格式化工具问题,静默降级,提交原代码
}
}
// 1. 构建提交数据
const data: SubmitCodePayload = {
problem_id: problem.value!.id,
language: codeStore.code.language,
code: codeStore.code.value,
}
if (contestID) {
data.contest_id = parseInt(contestID)
}
// 2. 提交代码到后端
const res = await submitCode(data)
console.log(`[Submit] 代码已提交: ID=${res.data.submission_id}`)
// 3. 启动冷却 + 监控
startCooldown()
startMonitoring(res.data.submission_id)
showResult.value = true
}
```
- [ ] **Step 3: 启动前后端 dev server 并手动验证**
Run两个终端:
```bash
cd OnlineJudge && python dev.py
cd ojnext && npm start
```
浏览器打开 `http://localhost:5173`,登录后进入任意题目页:
1. 选择 Python3编辑器中输入不规范格式的代码`x=1\ndef f( a,b ):\n return a+b`),点击"提交代码"。
Expected: 编辑器内容自动变为 ruff 格式化后的样式(如 `x = 1` 单独成行函数前空两行4 空格缩进),随后正常提交并显示判题结果。
2. 选择 C++,输入未格式化的代码(如 `#include<iostream>\nint main(){\nstd::cout<<"hi";\nreturn 0;}`),点击"提交代码"。
Expected: 编辑器内容自动变为 clang-format 格式化后的样式4 空格缩进,`{` 跟在行尾),随后正常提交。
3. 选择 Java提交任意代码。
Expected: 不触发格式化请求(浏览器 Network 面板中无 `/api/format_code` 请求),直接提交。
4. 可选Python3 下输入有语法错误但能通过 `checkPythonSyntax` 的边界代码,验证若后端返回 `format-error`,会弹出警告且不提交。
- [ ] **Step 4: Commit**
```bash
git add src/oj/problem/components/SubmitCode.vue
git commit -m "feat(problem): auto-format Python3/C/C++ code before submit"
```
---
## Self-Review Notes
- **Spec coverage:** 接口位置/参数/响应格式Task 1-4、依赖与 DockerTask 5-6、前端 API + 集成点 + 错误分类处理Task 7-8均已覆盖Java/Go/JS 跳过逻辑通过 `["python","c","cpp"].includes(formatLang)` 判断实现。
- **Type consistency:** `format_code(code, language)` 签名、`FormatSyntaxError`/`FormatToolError` 异常名在 Task 1/3 中一致;前端 `formatCode({code, language})` 返回 `{code: string}`,与 `res.data.code` 用法一致;`err` 取值 `"format-error"` / `"server-error"` 在后端 Task 3 与前端 Task 8 中一致。
- **Out of scope与 spec 一致,未在本计划中实现):** 格式化开关、Java/Go/JS 格式化、`CodeEditor.vue` 只读组件改动。