基于Python获取SonarQube的检查报告信息

1. 代码效果


2. 代码处理过程

2.1. 流程图

2.2. 流程图的Mermaid

复制代码
flowchart TD
    A([Start])
    A --> B[读取配置<br/>Sonar 地址 / 项目 Key / Token / 输出路径]

    B --> C[创建输出目录]

    C --> D[创建 Token 会话<br/>用于 Sonar Web API]
    D --> E{是否启用 Web 登录}

    E -->|是| F[执行 Web 登录<br/>获取会话 Cookie]
    E -->|否| G[跳过 Web 登录]

    F --> H[Web 会话可用]
    G --> H[仅使用 Token 会话]

    H --> I[获取质量门禁状态]
    H --> J[获取项目指标]
    H --> K[分页获取未解决问题列表]

    I --> L[初始化缓存<br/>规则缓存 / 源码缓存]
    J --> L
    K --> L

    L --> M[生成 Markdown 报告]

    M --> N[写入 Markdown 文件]

    %% ========== Markdown 内部 ==========
    subgraph S1[生成 Markdown 报告的内部流程]
        S1a[遍历每一条 Issue]
        S1a --> S1b[获取规则详情<br/>规则结果缓存]
        S1a --> S1c[获取代码片段]
    end

    M --> S1

    %% ========== 代码片段策略 ==========
    subgraph S2[代码片段获取策略]
        T1{Web 会话是否可用}
        T1 -->|是| T2[调用 issue_snippets 接口]
        T2 --> T3{是否成功}
        T3 -->|是| T4[解析 UI 同款代码片段]
        T3 -->|否| T5[降级使用源码接口]

        T1 -->|否| T5

        T5 --> T6[调用 sources/show 接口]
        T6 --> T7[截取违规行前后 N 行]
    end

    S1c --> S2

    N --> P[为 HTML 预计算所有代码片段]
    P --> Q[生成 HTML 报告]
    Q --> R[写入 HTML 文件]

    R --> S[输出原始 JSON 数据]
    S --> Z([End])

3. 代码内容

3.1. 注意事项

  • 注意修改
    • SONAR_HOST = "http://192.168.152.134:9000"
    • SONAR_USER = "admin"
    • SONAR_PASS = "admin123456"
    • PROJECT_KEY = "java_tool_geotoolsinput_b98089cb-632a-41c8-adc0-0bba35ed9f54"
    • BRANCH = None # 如需分支: "main"
    • SONAR_TOKEN = "sqp_437d1cb4b65080bad63f49a4ae244c82b4ba1390"

3.2. 代码内容

python 复制代码
# -*- coding: utf-8 -*-
import math
import json
import html
import re
from html import unescape
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Any, Tuple

import requests

# ======================================================
# 配置(⚠️ 技术可研:暂时写死)
# ======================================================
SONAR_HOST = "http://192.168.152.134:9000"


# ✅ Web 登录(用于拉 UI 同款 issue_snippets)
SONAR_USER = "admin"
SONAR_PASS = "admin123456"

PROJECT_KEY = "java_tool_geotoolsinput_b98089cb-632a-41c8-adc0-0bba35ed9f54"
BRANCH = None  # 如需分支: "main"

# ✅ Token 通道(稳定,可拉指标/问题/规则等)
SONAR_TOKEN = "sqp_437d1cb4b65080bad63f49a4ae244c82b4ba1390"

OUT_DIR = Path(".")
MD_PATH = OUT_DIR / "sonar-report.zh-CN.md"
HTML_PATH = OUT_DIR / "sonar-report.zh-CN.html"
JSON_PATH = OUT_DIR / "sonar-report.raw.json"

# 代码片段上下文行数:违规行前后 N 行(你要的 ±5)
SNIPPET_CONTEXT = 5

# ======================================================
# 中文映射
# ======================================================
SEVERITY_CN = {
    "BLOCKER": "阻断",
    "CRITICAL": "严重",
    "MAJOR": "主要",
    "MINOR": "次要",
    "INFO": "提示",
}

TYPE_CN = {
    "BUG": "缺陷",
    "VULNERABILITY": "漏洞",
    "CODE_SMELL": "代码异味",
    "SECURITY_HOTSPOT": "安全热点",
}

QUALITY_CN = {
    "RELIABILITY": "可靠性",
    "SECURITY": "安全性",
    "MAINTAINABILITY": "可维护性",
    "reliability": "可靠性",
    "security": "安全性",
    "maintainability": "可维护性",
}

IMPACT_LEVEL_CN = {
    "LOW": "低",
    "MEDIUM": "中",
    "HIGH": "高",
    "low": "低",
    "medium": "中",
    "high": "高",
}

# ======================================================
# Snippet HTML 处理(issue_snippets 返回的 code 带 <span> + HTML entity)
# ======================================================
SPAN_TAG_RE = re.compile(r"</?span[^>]*>")

def snippet_html_to_plain(text: str) -> str:
    if not text:
        return ""
    text = SPAN_TAG_RE.sub("", text)
    text = unescape(text)
    return text

# ======================================================
# HTTP 基础
# ======================================================
def api_get(session: requests.Session, path: str, params: Optional[dict] = None) -> dict:
    url = f"{SONAR_HOST}{path}"
    r = session.get(url, params=params, timeout=60)
    if r.status_code in (401, 403):
        raise RuntimeError(f"[ERROR] GET 鉴权失败({r.status_code}):{url}\n{r.text[:500]}")
    r.raise_for_status()
    return r.json()

def get_cookie(session: requests.Session, name: str) -> Optional[str]:
    for c in session.cookies:
        if c.name == name:
            return c.value
    return None

# ======================================================
# Token 通道 API(稳定)
# ======================================================
def fetch_quality_gate(session: requests.Session) -> dict:
    params = {"projectKey": PROJECT_KEY}
    if BRANCH:
        params["branch"] = BRANCH
    return api_get(session, "/api/qualitygates/project_status", params)

def fetch_measures(session: requests.Session) -> dict:
    metrics = ",".join(
        [
            "bugs",
            "vulnerabilities",
            "code_smells",
            "security_hotspots",
            "coverage",
            "duplicated_lines_density",
            "ncloc",
            "reliability_rating",
            "security_rating",
            "sqale_rating",
        ]
    )
    params = {"component": PROJECT_KEY, "metricKeys": metrics}
    if BRANCH:
        params["branch"] = BRANCH
    return api_get(session, "/api/measures/component", params)

def fetch_all_issues(session: requests.Session) -> List[dict]:
    issues: List[dict] = []
    params = {
        "componentKeys": PROJECT_KEY,
        "resolved": "false",
        "ps": 500,
        "p": 1,
    }
    if BRANCH:
        params["branch"] = BRANCH

    first = api_get(session, "/api/issues/search", params)
    total = int(first.get("total", 0))
    issues.extend(first.get("issues", []))

    pages = max(1, math.ceil(total / 500))
    for p in range(2, pages + 1):
        params["p"] = p
        data = api_get(session, "/api/issues/search", params)
        issues.extend(data.get("issues", []))

    return issues

def fetch_rule_detail(session: requests.Session, rule_key: str, cache: Dict[str, dict]) -> dict:
    if rule_key in cache:
        return cache[rule_key]
    data = api_get(session, "/api/rules/show", {"key": rule_key})
    rule = data.get("rule", {}) or {}
    cache[rule_key] = rule
    return rule

# sources/show:降级方案(拿纯源码行)
def fetch_sources_lines(session: requests.Session, component_key: str, cache: Dict[str, List[dict]]) -> List[dict]:
    if not component_key:
        return []
    if component_key in cache:
        return cache[component_key]
    try:
        data = api_get(session, "/api/sources/show", {"component": component_key})
        lines = data.get("sources", []) or []
        cache[component_key] = lines
        return lines
    except Exception:
        cache[component_key] = []
        return []

def fetch_code_snippet_by_sources_show(
    session: requests.Session,
    component_key: str,
    line: Optional[int],
    context: int,
    sources_cache: Dict[str, List[dict]],
) -> str:
    if not component_key or not line:
        return ""
    sources = fetch_sources_lines(session, component_key, sources_cache)
    if not sources:
        return ""

    start = max(1, int(line) - context)
    end = int(line) + context

    snippet_lines = []
    for s in sources:
        ln = s.get("line")
        code = s.get("code", "")
        if ln is None:
            continue
        ln = int(ln)
        if start <= ln <= end:
            prefix = ">>" if ln == int(line) else "  "
            snippet_lines.append(f"{prefix}{ln:4d}: {code}")
    return "\n".join(snippet_lines)

# ======================================================
# Web 登录 + issue_snippets(UI 同款代码块)
# ======================================================
def sonar_login_web(web_session: requests.Session) -> None:
    url = f"{SONAR_HOST}/api/authentication/login"
    headers = {
        "Accept": "application/json",
        "Content-Type": "application/x-www-form-urlencoded",
    }
    data = {"login": SONAR_USER, "password": SONAR_PASS}
    r = web_session.post(url, headers=headers, data=data, timeout=60)
    if r.status_code not in (200, 204):
        raise RuntimeError(f"登录失败: HTTP {r.status_code}, body={r.text[:300]}")

    jwt = get_cookie(web_session, "JWT-SESSION")
    if not jwt:
        raise RuntimeError("登录后未拿到 JWT-SESSION Cookie(可能登录失败或被策略拦截)")

    # XSRF 有时会一起下发;没有的话,后续也许仍能用,或者你可先 GET 一次页面触发
    xsrf = get_cookie(web_session, "XSRF-TOKEN")
    if not xsrf:
        # 触发一下页面,某些环境会在这里补发 XSRF
        try:
            web_session.get(f"{SONAR_HOST}/sessions/new", timeout=30)
        except Exception:
            pass

def fetch_issue_snippets_web(web_session: requests.Session, issue_key: str) -> Dict[str, Any]:
    url = f"{SONAR_HOST}/api/sources/issue_snippets"
    params = {"issueKey": issue_key}

    xsrf = get_cookie(web_session, "XSRF-TOKEN") or ""
    headers = {"Accept": "application/json"}
    if xsrf:
        headers["X-Xsrf-Token"] = xsrf

    r = web_session.get(url, params=params, headers=headers, timeout=60)
    if r.status_code == 403:
        raise RuntimeError("403 Forbidden:issue_snippets 需要 Cookie 会话 + XSRF,或权限不足。")
    r.raise_for_status()
    return r.json()

def issue_snippets_payload_to_text_block(payload: Dict[str, Any]) -> Tuple[str, str]:
    """
    把 issue_snippets 单文件 payload 转成纯文本代码块(含行号)
    返回:(path, code_text)
    """
    comp = payload.get("component", {}) or {}
    path = comp.get("path") or comp.get("name") or (comp.get("key") or "")
    lines = []
    for row in payload.get("sources", []) or []:
        ln = row.get("line")
        code_html = row.get("code", "")
        code_plain = snippet_html_to_plain(code_html)
        if isinstance(ln, int):
            lines.append(f"{ln:4d}: {code_plain}")
        else:
            lines.append(code_plain)
    return path, "\n".join(lines)

# ======================================================
# Snippet 统一入口:优先 issue_snippets,失败降级 sources/show
# ======================================================
def guess_code_lang(component_key: str) -> str:
    ck = (component_key or "").lower()
    if ck.endswith(".java"):
        return "java"
    if ck.endswith(".xml"):
        return "xml"
    if ck.endswith(".yml") or ck.endswith(".yaml"):
        return "yaml"
    if ck.endswith(".sql"):
        return "sql"
    return ""

def get_issue_code_snippet(
    issue: dict,
    web_session: Optional[requests.Session],
    token_session: requests.Session,
    sources_cache: Dict[str, List[dict]],
) -> Tuple[str, str]:
    """
    返回 (lang, snippet_text)
    - 优先:issue_snippets(web_session)
    - 降级:sources/show(token_session,±SNIPPET_CONTEXT)
    """
    component = issue.get("component", "") or ""
    line_no = issue.get("line")
    issue_key = issue.get("key", "") or ""

    # 1) 优先 issue_snippets(UI 同款)
    if web_session is not None and issue_key:
        try:
            snips = fetch_issue_snippets_web(web_session, issue_key)
            # 返回结构:{ "<componentKey>": {component:{...}, sources:[...]} }
            # 通常只有一个 key;取第一个即可
            for _k, payload in snips.items():
                path, code_text = issue_snippets_payload_to_text_block(payload)
                # issue_snippets 自带"范围行",通常已经类似截图内容
                lang = guess_code_lang(path)
                return lang, code_text
        except Exception:
            # 静默降级
            pass

    # 2) 降级 sources/show(前后 N 行)
    if component and line_no:
        code = fetch_code_snippet_by_sources_show(
            session=token_session,
            component_key=component,
            line=int(line_no),
            context=SNIPPET_CONTEXT,
            sources_cache=sources_cache,
        )
        lang = guess_code_lang(component)
        return lang, code

    return "", ""

# ======================================================
# 格式化工具
# ======================================================
def cn_quality_gate(status: str) -> str:
    if status == "OK":
        return "通过"
    if status == "ERROR":
        return "未通过"
    return status or "未知"

def safe_md(text: str) -> str:
    if text is None:
        return ""
    return str(text).replace("\n", " ").replace("|", "\\|").strip()

def safe_html(text: str) -> str:
    return html.escape("" if text is None else str(text))

def issue_ui_link(issue_key: str) -> str:
    return f"{SONAR_HOST}/project/issues?id={PROJECT_KEY}&open={issue_key}"

def rule_ui_link(rule_key: str) -> str:
    return f"{SONAR_HOST}/coding_rules?open={rule_key}"

def format_impacts(rule: dict) -> str:
    impacts = rule.get("impacts")
    if not impacts:
        return "---"
    parts = []
    if isinstance(impacts, dict):
        for k, v in impacts.items():
            q = QUALITY_CN.get(k, str(k) or "")
            lv = IMPACT_LEVEL_CN.get(v, str(v) or "")
            if q:
                parts.append(f"{q}·{lv}")
        return "、".join(parts) if parts else "---"
    if isinstance(impacts, list):
        for item in impacts:
            if not isinstance(item, dict):
                continue
            qk = item.get("softwareQuality") or item.get("quality") or item.get("key")
            lv = item.get("severity") or item.get("level") or item.get("impact")
            q = QUALITY_CN.get(qk, str(qk) if qk else "")
            lv_cn = IMPACT_LEVEL_CN.get(lv, str(lv) if lv else "")
            if q:
                parts.append(f"{q}·{lv_cn}")
        return "、".join(parts) if parts else "---"
    return "---"

def format_remediation(rule: dict) -> str:
    base = rule.get("remFnBaseEffort")
    rtype = rule.get("remFnType")
    gap = rule.get("remFnGapMultiplier")
    coeff = rule.get("debtRemFnCoeff")
    if base:
        return f"{base}"
    if isinstance(coeff, str) and coeff.strip():
        return coeff.strip()
    if rtype or gap:
        return f"{rtype or ''}{(' / ' + gap) if gap else ''}".strip(" /")
    return "---"

def strip_html_to_text(s: str) -> str:
    if not s:
        return ""
    s = re.sub(r"<br\s*/?>", " ", s, flags=re.IGNORECASE)
    s = re.sub(r"</p\s*>", " ", s, flags=re.IGNORECASE)
    s = re.sub(r"<[^>]+>", "", s)
    return re.sub(r"\s+", " ", s).strip()

# ======================================================
# Markdown 报告
# ======================================================
def build_markdown_report(
    qg: dict,
    measures: dict,
    issues: List[dict],
    rule_cache: Dict[str, dict],
    token_session: requests.Session,
    web_session: Optional[requests.Session],
    sources_cache: Dict[str, List[dict]],
) -> str:
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    qg_status = qg.get("projectStatus", {}).get("status", "UNKNOWN")

    metric_map = {}
    for m in measures.get("component", {}).get("measures", []) or []:
        metric_map[m.get("metric")] = m.get("value")

    def mv(k: str) -> str:
        return str(metric_map.get(k, ""))

    lines = []
    lines.append("# SonarQube 检查报告(中文)")
    lines.append("")
    lines.append(f"- 项目 Key:`{PROJECT_KEY}`")
    if BRANCH:
        lines.append(f"- 分支:`{BRANCH}`")
    lines.append(f"- 生成时间:`{now}`")
    lines.append(f"- SonarQube:`{SONAR_HOST}`")
    lines.append(f"- 质量门禁:**{cn_quality_gate(qg_status)}**")
    lines.append("")

    lines.append("## 指标概览")
    lines.append("")
    lines.append("| 指标 | 数值 |")
    lines.append("|---|---:|")
    lines.append(f"| Bugs(缺陷) | {mv('bugs')} |")
    lines.append(f"| Vulnerabilities(漏洞) | {mv('vulnerabilities')} |")
    lines.append(f"| Code Smells(代码异味) | {mv('code_smells')} |")
    lines.append(f"| Security Hotspots(安全热点) | {mv('security_hotspots')} |")
    lines.append(f"| Coverage(覆盖率%) | {mv('coverage')} |")
    lines.append(f"| Duplications(重复率%) | {mv('duplicated_lines_density')} |")
    lines.append(f"| NCLOC(有效代码行) | {mv('ncloc')} |")
    lines.append("")

    conds = qg.get("projectStatus", {}).get("conditions", []) or []
    if conds:
        lines.append("## 质量门禁条件(Quality Gate Conditions)")
        lines.append("")
        lines.append("| 指标 | 条件 | 阈值 | 实际值 | 状态 |")
        lines.append("|---|---|---:|---:|---|")
        for c in conds:
            metric = c.get("metricKey", "")
            op = c.get("operator", "")
            thr = c.get("errorThreshold", "")
            actual = c.get("actualValue", "")
            st = c.get("status", "")
            lines.append(f"| {metric} | {op} | {thr} | {actual} | {st} |")
        lines.append("")

    lines.append("## 问题清单(未解决)")
    lines.append(f"问题总数:**{len(issues)}**")
    lines.append("")
    lines.append("| 严重性 | 类型 | 规则(可点) | 影响(软件质量) | 修复成本 | 文件 | 行号 | 标题/说明 |")
    lines.append("|---|---|---|---|---|---|---:|---|")

    for it in issues:
        rule_key = it.get("rule", "")
        rule = fetch_rule_detail(token_session, rule_key, rule_cache) if rule_key else {}
        severity_cn = SEVERITY_CN.get(it.get("severity"), it.get("severity") or "")
        type_cn = TYPE_CN.get(it.get("type"), it.get("type") or "")
        rule_name = rule.get("name", "") or ""
        impacts_cn = format_impacts(rule)
        remediation = format_remediation(rule)
        component = it.get("component", "") or ""
        line_no = it.get("line") or ""
        message = it.get("message", "") or ""
        rule_link = rule_ui_link(rule_key) if rule_key else ""
        rule_cell = f"[`{safe_md(rule_key)}`]({rule_link})"
        if rule_name:
            rule_cell += f"<br/>{safe_md(rule_name)}"
        lines.append(
            "| {sev} | {typ} | {rule} | {imp} | {rem} | `{file}` | {line} | {msg} |".format(
                sev=safe_md(severity_cn),
                typ=safe_md(type_cn),
                rule=rule_cell,
                imp=safe_md(impacts_cn),
                rem=safe_md(remediation),
                file=safe_md(component),
                line=safe_md(line_no),
                msg=safe_md(message),
            )
        )

    lines.append("")
    lines.append(f"## 问题详情(含代码片段:优先 UI 同款,否则降级为前后 {SNIPPET_CONTEXT} 行)")
    lines.append("")

    for idx, it in enumerate(issues, start=1):
        rule_key = it.get("rule", "")
        rule = fetch_rule_detail(token_session, rule_key, rule_cache) if rule_key else {}
        severity_cn = SEVERITY_CN.get(it.get("severity"), it.get("severity") or "")
        type_cn = TYPE_CN.get(it.get("type"), it.get("type") or "")
        rule_name = rule.get("name", "") or ""
        impacts_cn = format_impacts(rule)
        remediation = format_remediation(rule)
        component = it.get("component", "") or ""
        line_no = it.get("line")
        message = it.get("message", "") or ""
        issue_key = it.get("key", "") or ""

        lines.append(f"### {idx}. {safe_md(message)}")
        lines.append(f"- 严重性:{safe_md(severity_cn)}")
        lines.append(f"- 类型:{safe_md(type_cn)}")
        lines.append(f"- 规则:`{safe_md(rule_key)}` {safe_md(rule_name)}")
        lines.append(f"- 影响:{safe_md(impacts_cn)}")
        lines.append(f"- 修复成本:{safe_md(remediation)}")
        lines.append(f"- 位置:`{safe_md(component)}` 行 {line_no if line_no else ''}")
        if issue_key:
            lines.append(f"- Sonar 链接:{issue_ui_link(issue_key)}")
        lines.append("")

        lang, snippet = get_issue_code_snippet(it, web_session, token_session, sources_cache)
        if snippet:
            # issue_snippets 通常范围更大;sources/show 是 ±N 行并带 >> 标记
            lines.append(f"```{lang}".rstrip())
            lines.append(snippet)
            lines.append("```")
        else:
            lines.append("> (无法获取源码片段:可能权限不足,或分析未保存源码。)")
        lines.append("")

    lines.append("## 规则说明(抽取本次用到的规则)")
    lines.append("")
    used_rules = sorted({it.get("rule") for it in issues if it.get("rule")})
    for rk in used_rules:
        rule = fetch_rule_detail(token_session, rk, rule_cache)
        name = rule.get("name", "") or ""
        impacts_cn = format_impacts(rule)
        remediation = format_remediation(rule)
        desc = strip_html_to_text(rule.get("htmlDesc", "") or "")
        desc_short = (desc[:220] + "...") if len(desc) > 220 else desc
        lines.append(f"### `{rk}` {name}")
        lines.append(f"- 影响:{impacts_cn}")
        lines.append(f"- 修复成本:{remediation}")
        if desc_short:
            lines.append(f"- 说明:{safe_md(desc_short)}")
        lines.append(f"- 规则链接:{rule_ui_link(rk)}")
        lines.append("")

    return "\n".join(lines)

# ======================================================
# HTML 报告(表格 + 内嵌代码片段)
# ======================================================
def build_html_report(md_text: str, issues: List[dict], rule_cache: Dict[str, dict], issue_snippets: Dict[str, str]) -> str:
    rows = []
    for it in issues:
        rule_key = it.get("rule", "") or ""
        rule = rule_cache.get(rule_key, {}) if rule_key else {}
        sev = SEVERITY_CN.get(it.get("severity"), it.get("severity") or "")
        typ = TYPE_CN.get(it.get("type"), it.get("type") or "")
        rule_name = rule.get("name", "") or ""
        impacts_cn = format_impacts(rule)
        remediation = format_remediation(rule)

        component = it.get("component", "") or ""
        line_no = it.get("line") or ""
        message = it.get("message", "") or ""
        issue_key = it.get("key", "") or ""
        issue_link = issue_ui_link(issue_key) if issue_key else ""
        rule_link = rule_ui_link(rule_key) if rule_key else ""

        rule_name_html = f"<div class='sub'>{safe_html(rule_name)}</div>" if rule_name else ""
        open_html = f"<a href='{safe_html(issue_link)}' target='_blank'>打开</a>" if issue_link else "---"

        code_block = issue_snippets.get(issue_key, "")
        code_html = f"<pre class='code'>{safe_html(code_block)}</pre>" if code_block else "<div class='small'>(无源码片段)</div>"

        row = (
            "<tr>"
            f"<td>{safe_html(sev)}</td>"
            f"<td>{safe_html(typ)}</td>"
            f"<td><a href='{safe_html(rule_link)}' target='_blank'>{safe_html(rule_key)}</a>{rule_name_html}</td>"
            f"<td>{safe_html(impacts_cn)}</td>"
            f"<td>{safe_html(remediation)}</td>"
            f"<td><code>{safe_html(component)}</code></td>"
            f"<td class='num'>{safe_html(line_no)}</td>"
            f"<td>{safe_html(message)}{code_html}</td>"
            f"<td>{open_html}</td>"
            "</tr>"
        )
        rows.append(row)

    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    branch_html = f" 分支:<code>{safe_html(BRANCH)}</code>" if BRANCH else ""

    return f"""<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>SonarQube 检查报告(中文)</title>
<style>
  body {{ font-family: system-ui, -apple-system, "Segoe UI", Arial, sans-serif; margin: 24px; color: #111; }}
  h1 {{ margin: 0 0 8px 0; }}
  .meta {{ color:#555; margin: 0 0 16px 0; line-height: 1.6; }}
  table {{ border-collapse: collapse; width: 100%; }}
  th, td {{ border: 1px solid #e5e7eb; padding: 10px; vertical-align: top; }}
  th {{ background: #f8fafc; text-align: left; }}
  td.num {{ text-align: right; white-space: nowrap; }}
  .sub {{ color:#555; margin-top: 4px; font-size: 12px; line-height: 1.4; }}
  .small {{ font-size: 12px; color:#666; }}
  .hr {{ height:1px; background:#eee; margin: 18px 0; }}
  details pre {{ white-space: pre-wrap; word-wrap: break-word; background:#f6f8fa; padding: 12px; border-radius: 10px; }}
  code {{ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-size: 12px; }}
  pre.code {{
    white-space: pre-wrap;
    word-wrap: break-word;
    background:#0b1020;
    color:#e6edf3;
    padding: 10px;
    border-radius: 10px;
    margin-top: 8px;
    font-size: 12px;
    line-height: 1.45;
  }}
</style>
</head>
<body>
  <h1>SonarQube 检查报告(中文)</h1>
  <div class="meta">
    <div>生成时间:<code>{safe_html(now)}</code></div>
    <div>项目 Key:<code>{safe_html(PROJECT_KEY)}</code>{branch_html}</div>
    <div>SonarQube:<code>{safe_html(SONAR_HOST)}</code></div>
    <div class="small">说明:本报告自动内嵌每条问题的源码片段(优先 UI issue_snippets,否则降级为 sources/show 前后 {SNIPPET_CONTEXT} 行)。</div>
  </div>

  <div class="hr"></div>

  <h2>问题清单(未解决)</h2>
  <div class="small">总计:{len(issues)} 条</div>
  <br/>

  <table>
    <thead>
      <tr>
        <th>严重性</th>
        <th>类型</th>
        <th>规则</th>
        <th>影响(软件质量)</th>
        <th>修复成本</th>
        <th>文件</th>
        <th>行号</th>
        <th>标题/说明(含代码片段)</th>
        <th>打开</th>
      </tr>
    </thead>
    <tbody>
      {''.join(rows) if rows else '<tr><td colspan="9">无未解决问题</td></tr>'}
    </tbody>
  </table>

  <div class="hr"></div>

  <details>
    <summary>展开:Markdown 原文(便于复制到 Obsidian)</summary>
    <pre>{safe_html(md_text)}</pre>
  </details>
</body>
</html>
"""

# ======================================================
# main
# ======================================================
def main():
    OUT_DIR.mkdir(parents=True, exist_ok=True)

    # 1) Token Session(稳定)
    token_session = requests.Session()
    token_session.auth = (SONAR_TOKEN, "")

    # 2) Web Session(用于 issue_snippets,可失败,失败则只用 token 降级)
    web_session: Optional[requests.Session] = None
    try:
        ws = requests.Session()
        sonar_login_web(ws)
        web_session = ws
        print("[INFO] Web 登录成功:将启用 issue_snippets 代码片段。")
    except Exception as e:
        print(f"[WARN] Web 登录失败,将降级仅用 Token 通道获取代码片段(sources/show):{e}")

    # 3) 拉取数据
    qg = fetch_quality_gate(token_session)
    measures = fetch_measures(token_session)
    issues = fetch_all_issues(token_session)

    rule_cache: Dict[str, dict] = {}
    sources_cache: Dict[str, List[dict]] = {}

    # 4) 生成 Markdown(内部会为每条 issue 拉 snippet)
    md = build_markdown_report(
        qg=qg,
        measures=measures,
        issues=issues,
        rule_cache=rule_cache,
        token_session=token_session,
        web_session=web_session,
        sources_cache=sources_cache,
    )
    MD_PATH.write_text(md, encoding="utf-8")

    # 5) 为 HTML 预计算每条 issue 的 snippet(按 issue_key 存)
    issue_snippets: Dict[str, str] = {}
    for it in issues:
        issue_key = it.get("key", "") or ""
        if not issue_key:
            continue
        lang, snippet = get_issue_code_snippet(it, web_session, token_session, sources_cache)
        if snippet:
            issue_snippets[issue_key] = snippet

    html_doc = build_html_report(md, issues, rule_cache, issue_snippets)
    HTML_PATH.write_text(html_doc, encoding="utf-8")

    # 6) Raw JSON
    JSON_PATH.write_text(
        json.dumps(
            {
                "projectKey": PROJECT_KEY,
                "branch": BRANCH,
                "sonarHost": SONAR_HOST,
                "qualityGate": qg,
                "measures": measures,
                "issues": issues,
                "rules": rule_cache,
                "snippetsContext": SNIPPET_CONTEXT,
                "snippetsSource": "issue_snippets(web) preferred, fallback sources/show(token)",
            },
            ensure_ascii=False,
            indent=2,
        ),
        encoding="utf-8",
    )

    print("✔ 报告生成完成:")
    print(f" - {MD_PATH.name}")
    print(f" - {HTML_PATH.name}")
    print(f" - {JSON_PATH.name}")

if __name__ == "__main__":
    main()
相关推荐
Boilermaker19921 小时前
[Java 并发编程] Synchronized 锁升级
java·开发语言
沈浩(种子思维作者)1 小时前
真的能精准医疗吗?癌症能提前发现吗?
人工智能·python·网络安全·健康医疗·量子计算
MM_MS1 小时前
Halcon变量控制类型、数据类型转换、字符串格式化、元组操作
开发语言·人工智能·深度学习·算法·目标检测·计算机视觉·视觉检测
꧁Q༒ོγ꧂2 小时前
LaTeX 语法入门指南
开发语言·latex
njsgcs2 小时前
ue python二次开发启动教程+ 导入fbx到指定文件夹
开发语言·python·unreal engine·ue
alonewolf_992 小时前
JDK17新特性全面解析:从语法革新到模块化革命
java·开发语言·jvm·jdk
io_T_T2 小时前
迭代器 iteration、iter 与 多线程 concurrent 交叉实践(详细)
python
古城小栈2 小时前
Rust 迭代器产出的引用层数——分水岭
开发语言·rust
华研前沿标杆游学2 小时前
2026年走进洛阳格力工厂参观游学
python
Carl_奕然3 小时前
【数据挖掘】数据挖掘必会技能之:A/B测试
人工智能·python·数据挖掘·数据分析