From 57ac9cdcd960dc41e81e9b327b4529537d27f16d Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 27 Aug 2026 15:41:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(swagger-detect):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=88=A4=E6=96=B7=E5=B0=88=E6=A1=88=E6=9C=89=E6=B2=92=E6=9C=89?= =?UTF-8?q?=E5=95=9F=E7=94=A8=20Swagger=20=E7=9A=84=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:新增 tools/swagger-detect.sh,判斷一個專案有沒有真的啟用 Swagger 文件。 涵蓋 dotnet、nodejs、python 三種技術棧,輸出 support=、stack=、package=、 config= 四類欄位;結束碼 0 支援、1 不支援、2 參數個數不對或專案路徑不存在。 Why:API 文件稽核只對產得出 Swagger 文件的專案有意義。支不支援如果交給 agent 自己看程式碼判斷,同一個專案可能這次說支援、下次說不支援。判定寫成腳本,呼叫端 讀結束碼分支就好,不必自己猜。 How:雙重確認,套件與設定缺一不算支援。第一關在套件宣告檔裡找已知的 Swagger 套件,第二關在原始碼裡找真的把 Swagger 接上去的呼叫或裝飾子。設定關鍵字一律 挑接線動作,不挑 import 或 require——光是引入套件不代表文件真的掛上去了。裝了 套件卻沒啟用的專案很常見,只看套件會誤判,讓稽核跑在一個根本產不出文件的專案 上。輸出刻意做成一行一個 key=value,呼叫端逐行讀就好。 Who:jsc-review 的 api-doc 技能,以及 jsc-sdlc 實作階段的收尾稽核。 --- tools/swagger-detect.sh | 170 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100755 tools/swagger-detect.sh diff --git a/tools/swagger-detect.sh b/tools/swagger-detect.sh new file mode 100755 index 0000000..0bdee25 --- /dev/null +++ b/tools/swagger-detect.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env sh +# swagger-detect.sh — 判斷一個專案有沒有真的啟用 Swagger(OpenAPI)文件。 +# 用法:swagger-detect.sh [專案路徑](預設目前目錄) +# +# 規則(雙重確認,缺一不算支援): +# 1. 套件:在套件宣告檔裡找得到已知的 Swagger 套件。 +# 2. 設定:在原始碼裡找得到實際把 Swagger 接上去的呼叫或裝飾子。 +# 只找到套件不算支援。裝了沒啟用的專案很常見,只看套件會誤判, +# 讓 api-doc 稽核跑在一個根本產不出文件的專案上。 +# 設定關鍵字一律挑「接線動作」,不挑 import 或 require。光是引入套件 +# 不代表文件真的掛上去了。 +# +# 涵蓋範圍: +# dotnet 套件 Swashbuckle.AspNetCore、NSwag.AspNetCore +# 設定 AddSwaggerGen、UseSwagger、AddOpenApiDocument、UseOpenApi +# nodejs 套件 swagger-ui-express、@nestjs/swagger、fastify-swagger(含 @fastify/swagger) +# 設定 SwaggerModule.setup、swaggerUi.setup、swaggerUi.serve、 +# register 進 fastify 的 swagger 外掛 +# python 套件 fastapi、flasgger、drf-spectacular +# 設定 FastAPI( 建立 app、Swagger( 掛上 flasgger、SPECTACULAR_SETTINGS、 +# SpectacularAPIView +# +# 輸出(純文字,一行一個 key=value,呼叫端逐行讀就好): +# 第一行永遠是 support=yes 或 support=no。 +# 之後每一組偵測到套件的技術棧輸出三行: +# stack=dotnet|nodejs|python +# package=命中的套件名,多個以半形逗號相連 +# config=命中的設定關鍵字,多個以半形逗號相連;沒命中就是空字串 +# 一個套件都沒找到時只有 support=no 一行。 +# 範例: +# support=yes +# stack=dotnet +# package=Swashbuckle.AspNetCore +# config=AddSwaggerGen,UseSwagger +# +# 結束碼: +# 0 支援(至少一個技術棧同時命中套件與設定) +# 1 不支援(沒有任何技術棧同時命中) +# 2 參數個數不對、專案路徑不存在,或環境缺 grep +# +# 護欄: +# 掃描一律跳過 node_modules、.git、bin、obj、dist、build、venv、.venv、 +# __pycache__、packages、vendor,避免把相依套件自己的原始碼當成專案設定。 +# 錯誤訊息一律印繁中到 stderr,正常輸出只走 stdout。 +set -u + +if [ "$#" -gt 1 ]; then + echo "用法:swagger-detect.sh [專案路徑]" >&2 + exit 2 +fi + +DIR="${1:-.}" + +[ -d "$DIR" ] || { echo "錯誤:找不到專案路徑 $DIR。請確認路徑後重試。" >&2; exit 2; } +command -v grep >/dev/null 2>&1 || { echo "錯誤:環境缺 grep,無法掃描。" >&2; exit 2; } + +EX1=--exclude-dir=node_modules +EX2=--exclude-dir=.git +EX3=--exclude-dir=bin +EX4=--exclude-dir=obj +EX5=--exclude-dir=dist +EX6=--exclude-dir=build +EX7=--exclude-dir=venv +EX8=--exclude-dir=.venv +EX9=--exclude-dir=__pycache__ +EX10=--exclude-dir=packages +EX11=--exclude-dir=vendor + +# scan <延伸正規表示式> <副檔名樣式…>:命中回 0,沒命中回 1。 +scan() { + pattern="$1" + shift + for inc in "$@"; do + if grep -R -l -E -i \ + "$EX1" "$EX2" "$EX3" "$EX4" "$EX5" "$EX6" \ + "$EX7" "$EX8" "$EX9" "$EX10" "$EX11" \ + --include="$inc" -e "$pattern" "$DIR" >/dev/null 2>&1; then + return 0 + fi + done + return 1 +} + +# append <既有清單> <新項目>:以半形逗號相連後印出。 +append() { + if [ -z "$1" ]; then + printf '%s' "$2" + else + printf '%s,%s' "$1" "$2" + fi +} + +SUPPORT=no +RECORDS="" + +# collect <技術棧> <套件清單> <設定清單>:有套件才留紀錄,兩者都有才算支援。 +collect() { + [ -n "$2" ] || return 0 + RECORDS="${RECORDS}stack=$1 +package=$2 +config=$3 +" + [ -n "$3" ] && SUPPORT=yes + return 0 +} + +# ── dotnet ──────────────────────────────────────────────────────────────── +DOTNET_PKG="" +DOTNET_CFG="" +for pkg in 'Swashbuckle\.AspNetCore:Swashbuckle.AspNetCore' 'NSwag\.AspNetCore:NSwag.AspNetCore'; do + if scan "${pkg%%:*}" '*.csproj' '*.fsproj' '*.vbproj' '*.props' 'packages.config'; then + DOTNET_PKG=$(append "$DOTNET_PKG" "${pkg##*:}") + fi +done +if [ -n "$DOTNET_PKG" ]; then + for cfg in AddSwaggerGen UseSwagger AddOpenApiDocument UseOpenApi; do + if scan "$cfg" '*.cs' '*.fs' '*.vb'; then + DOTNET_CFG=$(append "$DOTNET_CFG" "$cfg") + fi + done +fi +collect dotnet "$DOTNET_PKG" "$DOTNET_CFG" + +# ── nodejs ──────────────────────────────────────────────────────────────── +NODE_PKG="" +NODE_CFG="" +for pkg in swagger-ui-express @nestjs/swagger fastify-swagger @fastify/swagger; do + if scan "\"$pkg\"" 'package.json'; then + NODE_PKG=$(append "$NODE_PKG" "$pkg") + fi +done +if [ -n "$NODE_PKG" ]; then + # 每一項都是「接線動作」:掛路由或註冊外掛,不是 import。 + for cfg in 'SwaggerModule\.setup:SwaggerModule.setup' \ + 'swaggerUi\.setup:swaggerUi.setup' \ + 'swaggerUi\.serve:swaggerUi.serve' \ + 'register\(.*swagger:register(swagger)'; do + if scan "${cfg%%:*}" '*.js' '*.mjs' '*.cjs' '*.ts'; then + NODE_CFG=$(append "$NODE_CFG" "${cfg##*:}") + fi + done +fi +collect nodejs "$NODE_PKG" "$NODE_CFG" + +# ── python ──────────────────────────────────────────────────────────────── +PY_PKG="" +PY_CFG="" +for pkg in fastapi flasgger drf-spectacular; do + if scan "$pkg" 'requirements*.txt' 'pyproject.toml' 'Pipfile' 'setup.py' 'setup.cfg'; then + PY_PKG=$(append "$PY_PKG" "$pkg") + fi +done +if [ -n "$PY_PKG" ]; then + for cfg in 'FastAPI\(:FastAPI(' \ + 'Swagger\(:Swagger(' \ + 'SPECTACULAR_SETTINGS:SPECTACULAR_SETTINGS' \ + 'SpectacularAPIView:SpectacularAPIView'; do + if scan "${cfg%%:*}" '*.py'; then + PY_CFG=$(append "$PY_CFG" "${cfg##*:}") + fi + done +fi +collect python "$PY_PKG" "$PY_CFG" + +# ── 輸出 ────────────────────────────────────────────────────────────────── +echo "support=$SUPPORT" +[ -n "$RECORDS" ] && printf '%s' "$RECORDS" + +[ "$SUPPORT" = yes ] && exit 0 +exit 1