From a8c10af0442c06cd10dc8a281d61706120a709a2 Mon Sep 17 00:00:00 2001 From: Ivan Hanloth Date: Fri, 17 Jul 2026 00:17:55 +0100 Subject: [PATCH 01/19] =?UTF-8?q?feat:=20=E7=BD=AE=E7=A9=BA=E7=83=AD?= =?UTF-8?q?=E9=94=AE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/config/dist/index.html | 116 ------------------ .../ui/src/components/HotkeyRecorder.svelte | 17 ++- crates/core/src/agent.rs | 6 +- crates/core/src/hotkey.rs | 13 ++ docs/guide/hotkeys.md | 2 + 5 files changed, 35 insertions(+), 119 deletions(-) delete mode 100644 apps/config/dist/index.html diff --git a/apps/config/dist/index.html b/apps/config/dist/index.html deleted file mode 100644 index 5aa12da..0000000 --- a/apps/config/dist/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - Boss Key 设置 - - - - - - - -
- -
- -
-
Boss Key 正在启动…
-
- - - diff --git a/apps/config/ui/src/components/HotkeyRecorder.svelte b/apps/config/ui/src/components/HotkeyRecorder.svelte index f58b416..0a68993 100644 --- a/apps/config/ui/src/components/HotkeyRecorder.svelte +++ b/apps/config/ui/src/components/HotkeyRecorder.svelte @@ -37,13 +37,23 @@ resumeMonitoring(REASON); } + function clear() { + stop(); + value = ""; + } + onDestroy(stop);
{label} - {recording ? "请按下组合键…" : value || "未设置"} - + + {recording ? "请按下组合键…" : value || "已关闭"} + + + {#if value && !recording} + + {/if}
diff --git a/apps/config/ui/src/components/Markdown.svelte b/apps/config/ui/src/components/Markdown.svelte new file mode 100644 index 0000000..2d865d7 --- /dev/null +++ b/apps/config/ui/src/components/Markdown.svelte @@ -0,0 +1,140 @@ + + + + +
e.preventDefault()}> + {@html html} +
+ + diff --git a/apps/config/ui/src/components/UpdateModal.svelte b/apps/config/ui/src/components/UpdateModal.svelte index 25e2f40..c9af80a 100644 --- a/apps/config/ui/src/components/UpdateModal.svelte +++ b/apps/config/ui/src/components/UpdateModal.svelte @@ -5,6 +5,7 @@ import IconDownload from "~icons/lucide/download"; import IconTriangleAlert from "~icons/lucide/triangle-alert"; import IconX from "~icons/lucide/x"; + import Markdown from "./Markdown.svelte"; import { win } from "../lib/ipc.js"; import { app, toast } from "../lib/state.svelte.js"; import { downloadUrl, formatTime, openExternal } from "../lib/verhub.js"; @@ -61,7 +62,7 @@ {#if target.title}

{target.title}

{/if} - {#if target.content}
{target.content}
{/if} + {#if target.content}
{/if} {#if forced}

{t("update.forcedNote")}

@@ -164,16 +165,11 @@ font-weight: 600; } .notes { - margin: 0; padding: 10px 12px; background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; - font-family: inherit; font-size: 12.5px; - line-height: 1.6; - white-space: pre-wrap; - word-break: break-word; max-height: 240px; overflow-y: auto; } diff --git a/apps/config/ui/src/lib/ipc.js b/apps/config/ui/src/lib/ipc.js index dfc5bce..e4f5973 100644 --- a/apps/config/ui/src/lib/ipc.js +++ b/apps/config/ui/src/lib/ipc.js @@ -147,7 +147,7 @@ function mockInvoke(cmd, args) { { id: "mock-1", title: "Boss Key 3.0 发布", - content: "全新界面与核心,鼠标按键触发、崩溃恢复、进程冻结。", + content: "全新界面与核心,**鼠标按键触发**、崩溃恢复、进程冻结。详见 [更新日志](https://boss-key.ivan-hanloth.cn/changelog/)。", is_pinned: true, is_hidden: false, author: "Ivan Hanloth", diff --git a/apps/config/ui/src/lib/markdown.js b/apps/config/ui/src/lib/markdown.js new file mode 100644 index 0000000..101eb92 --- /dev/null +++ b/apps/config/ui/src/lib/markdown.js @@ -0,0 +1,213 @@ +// 极简 Markdown 渲染,供公告与更新日志使用。只覆盖 GitHub 风格的常用子集, +// 不引入任何依赖。 +// +// 内容来自 Verhub 远端,因此这里**先转义再拼标签**:输出里出现的标签全部由本 +// 模块生成,源文本里的原始 HTML 只会被当成字面量显示,无需再过一遍消毒。 + +const ESCAPE = { "&": "&", "<": "<", ">": ">", '"': """ }; + +const FENCE = /^ {0,3}(`{3,}|~{3,})/; +const HR = /^ {0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/; +const HEADING = /^ {0,3}(#{1,6})[ \t]+(.*?)[ \t]*#*[ \t]*$/; +const QUOTE = /^ {0,3}> ?(.*)$/; +const ITEM = /^([ \t]*)(?:([-*+])|(\d{1,9})[.)])[ \t]+(.*)$/; +const TASK = /^\[([ xX])\][ \t]+/; + +// 行内解析的占位符哨兵。NUL 不会出现在正文里,也不属于 \s,因此裸链接之类的 +// 规则不会越过它去改写已经成型的标签。 +const SENTINEL = String.fromCharCode(0); +const PLACEHOLDER = new RegExp(`${SENTINEL}(\\d+)${SENTINEL}`, "g"); + +const escapeHtml = (s) => s.replace(/[&<>"]/g, (c) => ESCAPE[c]); + +/** 只放行 http(s) 与 mailto:javascript: / data: 等一律按纯文本处理。 */ +function safeUrl(raw) { + const url = raw.trim(); + return /^(?:https?:\/\/|mailto:)/i.test(url) ? url : null; +} + +const startsBlock = (line) => + FENCE.test(line) || HR.test(line) || HEADING.test(line) || QUOTE.test(line) || ITEM.test(line); + +/** 渲染行内标记,返回 HTML 片段。 */ +function inline(text) { + const stash = []; + // 已成型的标签先寄存成占位符,避免被后续规则二次改写。 + const hold = (html) => `${SENTINEL}${stash.push(html) - 1}${SENTINEL}`; + let s = escapeHtml(text); + + s = s.replace(/(`+)([^`]+?)\1/g, (_, __, code) => hold(`${code}`)); + + // CSP 只允许 self / data: 图源,远端图片加载不出来,统一退化成链接。 + s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (m, alt, dest) => anchor(dest, alt, hold) ?? m); + s = s.replace(/\[([^\]]*)\]\(([^)]+)\)/g, (m, label, dest) => anchor(dest, label, hold) ?? m); + + // 裸链接。前面必须是行首/空白/左括号,因此紧挨占位符的文本不会被重复包裹。 + s = s.replace(/(^|[\s(])(https?:\/\/\S+)/g, (_, pre, raw) => { + const trailing = raw.match(/[.,;:!?)]+$/)?.[0] ?? ""; + const url = raw.slice(0, raw.length - trailing.length); + return pre + hold(``) + url + hold("") + trailing; + }); + + s = s + .replace(/\*\*([^\s*](?:[^*]*[^\s*])?)\*\*/g, "$1") + .replace(/__([^\s_](?:[^_]*[^\s_])?)__/g, "$1") + .replace(/(^|[^*])\*([^\s*](?:[^*]*[^\s*])?)\*/g, "$1$2") + .replace(/(^|[^\w_])_([^\s_](?:[^_]*[^\s_])?)_/g, "$1$2") + .replace(/~~([^~]+)~~/g, "$1"); + + return s.replace(PLACEHOLDER, (_, i) => stash[+i]); +} + +/** 拼一个链接;目标协议不安全时返回 null,交由调用方原样保留。 */ +function anchor(dest, label, hold) { + const url = safeUrl(dest.split(/[ \t]/)[0]); + if (!url) return null; + return hold(``) + (label || url) + hold(""); +} + +/** 收集一段连续的列表行,返回 [html, 下一个未消费的行号]。 */ +function list(lines, start) { + const items = []; + let i = start; + while (i < lines.length) { + const m = lines[i].match(ITEM); + if (m) { + items.push({ + indent: m[1].replace(/\t/g, " ").length, + ordered: !!m[3], + start: m[3] ? Number(m[3]) : 0, + text: [m[4]], + }); + i++; + continue; + } + if (!lines[i].trim()) { + // 列表项之间允许空行,空行之后不再是列表项才算结束。 + if (lines[i + 1] && ITEM.test(lines[i + 1])) { + i++; + continue; + } + break; + } + if (items.length && /^[ \t]/.test(lines[i])) { + items[items.length - 1].text.push(lines[i].trim()); + i++; + continue; + } + break; + } + + let html = ""; + let pos = 0; + while (pos < items.length) { + const [chunk, next] = buildList(items, pos); + html += chunk; + pos = next; + } + return [html, i]; +} + +/** 把扁平的 items 按缩进还原成嵌套列表,返回 [html, 下一个未消费的下标]。 */ +function buildList(items, pos) { + const { indent, ordered, start } = items[pos]; + const tag = ordered ? "ol" : "ul"; + let html = ordered && start > 1 ? `
    ` : `<${tag}>`; + let i = pos; + + while (i < items.length && items[i].indent >= indent) { + if (items[i].indent > indent) { + const [sub, next] = buildList(items, i); + // 子列表挂进上一个
  1. 内部。 + html = html.endsWith("
  2. ") ? `${html.slice(0, -5)}${sub}` : html + sub; + i = next; + continue; + } + if (items[i].ordered !== ordered) break; // 换了列表类型,交给上层另起一个 + html += item(items[i]); + i++; + } + return [`${html}`, i]; +} + +function item(it) { + const task = it.text[0].match(TASK); + if (task) { + const rest = [it.text[0].slice(task[0].length), ...it.text.slice(1)]; + const checked = task[1] === " " ? "" : " checked"; + return `
  3. ${rest.map(inline).join("
    ")}
  4. `; + } + return `
  5. ${it.text.map(inline).join("
    ")}
  6. `; +} + +function blocks(lines) { + const out = []; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + + if (!line.trim()) { + i++; + continue; + } + + const fence = line.match(FENCE); + if (fence) { + const close = fence[1][0].repeat(3); + const body = []; + i++; + while (i < lines.length && !lines[i].trim().startsWith(close)) body.push(lines[i++]); + i++; // 吃掉收尾栅栏;栅栏缺失时正好越界结束 + out.push(`
    ${escapeHtml(body.join("\n"))}
    `); + continue; + } + + if (HR.test(line)) { + out.push("
    "); + i++; + continue; + } + + const h = line.match(HEADING); + if (h) { + out.push(`${inline(h[2])}`); + i++; + continue; + } + + if (QUOTE.test(line)) { + const body = []; + while (i < lines.length && lines[i].trim()) { + const m = lines[i].match(QUOTE); + body.push(m ? m[1] : lines[i]); // 无 > 前缀的续行也算引用内容 + i++; + } + out.push(`
    ${blocks(body)}
    `); + continue; + } + + if (ITEM.test(line)) { + const [html, next] = list(lines, i); + out.push(html); + i = next; + continue; + } + + // 段落。软换行按 GitHub 评论的习惯渲染成
    ,而不是并成一行。 + const para = []; + while (i < lines.length && lines[i].trim() && !startsBlock(lines[i])) { + para.push(lines[i].trim()); + i++; + } + out.push(`

    ${para.map(inline).join("
    ")}

    `); + } + + return out.join(""); +} + +/** 把 Markdown 源文本渲染成 HTML 字符串。 */ +export function renderMarkdown(source) { + if (!source) return ""; + return blocks(String(source).replace(/\r\n?/g, "\n").split("\n")); +} diff --git a/apps/config/ui/src/lib/markdown.test.js b/apps/config/ui/src/lib/markdown.test.js new file mode 100644 index 0000000..3a6bb41 --- /dev/null +++ b/apps/config/ui/src/lib/markdown.test.js @@ -0,0 +1,130 @@ +import { describe, expect, it } from "vitest"; +import { renderMarkdown } from "./markdown.js"; + +describe("renderMarkdown 块级", () => { + it("空输入返回空串", () => { + expect(renderMarkdown("")).toBe(""); + expect(renderMarkdown(null)).toBe(""); + expect(renderMarkdown(undefined)).toBe(""); + }); + + it("标题按级数渲染", () => { + expect(renderMarkdown("# 大标题")).toBe("

    大标题

    "); + expect(renderMarkdown("### 小标题")).toBe("

    小标题

    "); + expect(renderMarkdown("####### 七个井号不是标题")).toBe("

    ####### 七个井号不是标题

    "); + }); + + it("段落内的软换行渲染成
    ,空行分段", () => { + expect(renderMarkdown("第一行\n第二行\n\n另一段")).toBe("

    第一行
    第二行

    另一段

    "); + }); + + it("分割线", () => { + expect(renderMarkdown("---")).toBe("
    "); + expect(renderMarkdown("***")).toBe("
    "); + expect(renderMarkdown("--")).toBe("

    --

    "); + }); + + it("引用块内部继续解析", () => { + expect(renderMarkdown("> **重要**")).toBe("

    重要

    "); + }); + + it("围栏代码块原样保留、不解析行内标记", () => { + expect(renderMarkdown("```js\nconst a = **1**;\n```")).toBe("
    const a = **1**;
    "); + }); + + it("未闭合的围栏吃到结尾", () => { + expect(renderMarkdown("```\na\nb")).toBe("
    a\nb
    "); + }); +}); + +describe("renderMarkdown 列表", () => { + it("无序列表", () => { + expect(renderMarkdown("- 甲\n- 乙")).toBe(""); + }); + + it("有序列表,起始序号非 1 时带 start", () => { + expect(renderMarkdown("1. 甲\n2. 乙")).toBe("
    "); + expect(renderMarkdown("3. 丙")).toBe('
    '); + }); + + it("缩进还原成嵌套列表", () => { + expect(renderMarkdown("- 甲\n - 甲一\n- 乙")).toBe( + "", + ); + }); + + it("任务列表渲染成只读复选框", () => { + expect(renderMarkdown("- [x] 做完了\n- [ ] 还没做")).toBe( + '', + ); + }); + + it("列表结束后回到段落", () => { + expect(renderMarkdown("- 甲\n\n收尾")).toBe("

    收尾

    "); + }); +}); + +describe("renderMarkdown 行内", () => { + it("粗体、斜体、删除线、行内代码", () => { + expect(renderMarkdown("**粗** *斜* ~~删~~ `码`")).toBe( + "

    ", + ); + }); + + it("行内代码里的标记不再解析", () => { + expect(renderMarkdown("`**不是粗体**`")).toBe("

    **不是粗体**

    "); + }); + + it("蛇形命名里的下划线不当成斜体", () => { + expect(renderMarkdown("some_var_name")).toBe("

    some_var_name

    "); + }); + + it("链接与裸链接", () => { + expect(renderMarkdown("[官网](https://boss-key.ivan-hanloth.cn/)")).toBe( + '

    官网

    ', + ); + expect(renderMarkdown("见 https://example.com 。")).toBe( + '

    https://example.com

    ', + ); + }); + + it("裸链接不吞掉句末标点", () => { + expect(renderMarkdown("见 https://example.com.")).toBe( + '

    https://example.com.

    ', + ); + }); + + it("图片退化成链接(CSP 不允许远端图源)", () => { + expect(renderMarkdown("![截图](https://example.com/a.png)")).toBe( + '

    截图

    ', + ); + }); +}); + +describe("renderMarkdown 安全", () => { + it("原始 HTML 一律转义", () => { + expect(renderMarkdown("")).toBe( + "

    <script>alert(1)</script>

    ", + ); + expect(renderMarkdown('')).toBe( + "

    <img src=x onerror="alert(1)">

    ", + ); + }); + + it("非 http/mailto 协议的链接不生成 ", () => { + expect(renderMarkdown("[点我](javascript:alert(1))")).toBe("

    [点我](javascript:alert(1))

    "); + expect(renderMarkdown("[点我](data:text/html,
    @@ -27,6 +48,28 @@ {#snippet control()}{/snippet} + +
    +

    {t("notify.trayCard")}

    + {#each BADGE_COLORS as color (color.key)} + + {#snippet control()} +
    + + +
    + {/snippet} +
    + {/each} +
    {t("notify.trayPriorityNote")}
    + + {#snippet control()}{/snippet} + +
    diff --git a/apps/config/ui/src/lib/ipc.js b/apps/config/ui/src/lib/ipc.js index 1dbed7e..55d4ea3 100644 --- a/apps/config/ui/src/lib/ipc.js +++ b/apps/config/ui/src/lib/ipc.js @@ -29,6 +29,8 @@ const mockConfig = { hide_current: true, click_to_hide: true, hide_icon_after_hide: false, + tray_badges: { red: "hidden", green: "auto_hide", yellow: "hide_current", blue: "freeze" }, + tray_show_tooltip: true, freeze_after_hide: false, enhanced_freeze: false, freeze_whole_tree: false, @@ -96,6 +98,8 @@ const mockWindows = [ /** mock 下的核心监控状态。 */ let mockMonitoring = true; +/** mock 下的自动隐藏开关(随 save_config 更新,供状态轮询回读联动)。 */ +let mockAutoHide = mockConfig.setting.auto_hide_enabled; /** mock 下的开机自启状态(有状态,供预览联动 UI)。 */ let mockAutostart = false; @@ -110,7 +114,16 @@ function mockInvoke(cmd, args) { case "autostart_status": return { enabled: mockAutostart, method: mockAutostart ? "task" : null }; case "core_status": - return { running: true, hidden: false, elevated: false, monitoring: mockMonitoring }; + return { + running: true, + hidden: false, + elevated: false, + monitoring: mockMonitoring, + auto_hide_enabled: mockAutoHide, + }; + case "save_config": + mockAutoHide = !!args?.config?.setting?.auto_hide_enabled; + return null; case "set_hotkeys_enabled": mockMonitoring = !!args?.enabled; return true; diff --git a/apps/config/ui/src/lib/state.svelte.js b/apps/config/ui/src/lib/state.svelte.js index c306df6..9038ebd 100644 --- a/apps/config/ui/src/lib/state.svelte.js +++ b/apps/config/ui/src/lib/state.svelte.js @@ -18,7 +18,7 @@ export const app = $state({ /** exe 路径 → PNG data URI;null 表示查询过但无图标(负缓存)。 */ icons: {}, /** 核心状态;running 为 null 表示首次检测尚未返回。monitoring 由核心回报。 */ - status: { running: null, hidden: false, elevated: false, monitoring: true }, + status: { running: null, hidden: false, elevated: false, monitoring: true, auto_hide_enabled: false }, autostart: false, /** 当前自启注册方式:"task"|"registry"|null(未注册)。 */ autostartMethod: null, @@ -173,11 +173,15 @@ async function loadIcons(windows) { // 自动保存:改动即存,带 debounce。 let saveTimer = null; +/** 有改动排队待存(debounce 期间为 true);状态回读不得覆盖未保存的改动。 */ +let savePending = false; /** 安排一次自动保存;连续改动只在停顿后写一次盘。 */ export function scheduleSave(delayMs = 600) { + savePending = true; clearTimeout(saveTimer); saveTimer = setTimeout(() => { + savePending = false; saveConfig(); }, delayMs); } @@ -287,13 +291,23 @@ export function reportError(message, detail = "") { app.errorReport = { message, detail: String(detail) }; } +/** 托盘菜单也能切换自动隐藏;以核心回报为准回读界面,但不覆盖用户尚未保存的改动。 */ +function syncAutoHideFromCore() { + if (!app.config || !app.status.running) return; + if (savePending || app.saving) return; + if (app.config.setting.auto_hide_enabled !== app.status.auto_hide_enabled) { + app.config.setting.auto_hide_enabled = app.status.auto_hide_enabled; + } +} + /** 刷新核心状态;失败时视为核心离线。 */ export async function refreshStatus() { try { app.status = await invoke("core_status"); } catch { - app.status = { running: false, hidden: false, elevated: false, monitoring: false }; + app.status = { running: false, hidden: false, elevated: false, monitoring: false, auto_hide_enabled: false }; } + syncAutoHideFromCore(); // 核心在停用期间重启过(新实例默认监听),重新按下停用。 if (suspenders.size > 0 && app.status.running && app.status.monitoring) { invoke("set_hotkeys_enabled", { enabled: false }).catch(() => {}); diff --git a/apps/config/ui/src/locales/en.js b/apps/config/ui/src/locales/en.js index 183cd16..4a38e9f 100644 --- a/apps/config/ui/src/locales/en.js +++ b/apps/config/ui/src/locales/en.js @@ -6,7 +6,7 @@ export default { "tab.binding": "Windows", "tab.hotkeys": "Hotkeys & Mouse", - "tab.notify": "Notifications", + "tab.notify": "Alerts", "tab.options": "Options", "tab.about": "About & Feedback", @@ -175,6 +175,24 @@ export default { "notify.onShow": "Notify when showing windows", "notify.onShowDesc": "Show a notification each time windows are restored", + "notify.trayCard": "Tray icon status", + "notify.badgeRed": "Red", + "notify.badgeGreen": "Green", + "notify.badgeYellow": "Yellow", + "notify.badgeBlue": "Blue", + "notify.statusNone": "Do not show", + "notify.statusHidden": "Windows are hidden", + "notify.statusAutoHide": "Auto hide is enabled", + "notify.statusHideCurrent": "Also-hide-active-window is enabled", + "notify.statusFreeze": "Process freezing is enabled", + "notify.statusElevated": "Running as administrator", + "notify.statusMonitorPaused": "Hotkey monitoring is paused", + "notify.trayPriorityNote": + "Bind a state to each colored dot badge; the badge appears in the bottom-right corner of the tray icon. When several bound states are active at once, only one dot is shown, in red > green > yellow > blue priority order. Choose “Do not show” to disable a color.", + "notify.trayTooltip": "Show the tray icon tooltip", + "notify.trayTooltipDesc": + "Show “Boss Key” when hovering over the tray icon; turn this off to show no text for extra discretion.", + "options.generalCard": "General", "options.muteAfterHide": "Mute after hiding", "options.muteAfterHideDesc": diff --git a/apps/config/ui/src/locales/zh-CN.js b/apps/config/ui/src/locales/zh-CN.js index 083eed5..b8afbaa 100644 --- a/apps/config/ui/src/locales/zh-CN.js +++ b/apps/config/ui/src/locales/zh-CN.js @@ -7,7 +7,7 @@ export default { "tab.binding": "窗口绑定", "tab.hotkeys": "热键与鼠标", - "tab.notify": "通知设置", + "tab.notify": "提示设置", "tab.options": "其他选项", "tab.about": "关于与反馈", @@ -171,6 +171,23 @@ export default { "notify.onShow": "显示窗口时通知", "notify.onShowDesc": "每次恢复显示时弹出通知", + "notify.trayCard": "图标状态提示", + "notify.badgeRed": "红色", + "notify.badgeGreen": "绿色", + "notify.badgeYellow": "黄色", + "notify.badgeBlue": "蓝色", + "notify.statusNone": "不显示", + "notify.statusHidden": "存在隐藏中的窗口", + "notify.statusAutoHide": "启用了自动隐藏", + "notify.statusHideCurrent": "启用了同时隐藏当前窗口", + "notify.statusFreeze": "启用了进程冻结", + "notify.statusElevated": "以管理员身份运行", + "notify.statusMonitorPaused": "热键监控已暂停", + "notify.trayPriorityNote": + "为每种颜色的圆点角标绑定一个状态,角标显示在托盘图标右下角;多个状态同时满足时,按红 > 绿 > 黄 > 蓝的优先级只显示一个圆点。选择「不显示」可置空该颜色。", + "notify.trayTooltip": "显示图标悬浮名称", + "notify.trayTooltipDesc": "鼠标悬停在托盘图标上时显示「Boss Key」;关闭后不显示任何文字,更隐蔽。", + "options.generalCard": "常规", "options.muteAfterHide": "隐藏窗口后静音", "options.muteAfterHideDesc": "隐藏时静音目标进程的音频,恢复显示时自动取消静音。", diff --git a/apps/config/ui/src/locales/zh-TW.js b/apps/config/ui/src/locales/zh-TW.js index 1e3ce1d..4ce2a6a 100644 --- a/apps/config/ui/src/locales/zh-TW.js +++ b/apps/config/ui/src/locales/zh-TW.js @@ -6,7 +6,7 @@ export default { "tab.binding": "視窗綁定", "tab.hotkeys": "快速鍵與滑鼠", - "tab.notify": "通知設定", + "tab.notify": "提示設定", "tab.options": "其他選項", "tab.about": "關於與意見回饋", @@ -170,6 +170,23 @@ export default { "notify.onShow": "顯示視窗時通知", "notify.onShowDesc": "每次復原顯示時顯示通知", + "notify.trayCard": "圖示狀態提示", + "notify.badgeRed": "紅色", + "notify.badgeGreen": "綠色", + "notify.badgeYellow": "黃色", + "notify.badgeBlue": "藍色", + "notify.statusNone": "不顯示", + "notify.statusHidden": "存在隱藏中的視窗", + "notify.statusAutoHide": "已啟用自動隱藏", + "notify.statusHideCurrent": "已啟用同時隱藏目前視窗", + "notify.statusFreeze": "已啟用程序凍結", + "notify.statusElevated": "以系統管理員身分執行", + "notify.statusMonitorPaused": "快速鍵監控已暫停", + "notify.trayPriorityNote": + "為每種顏色的圓點角標繫結一個狀態,角標顯示在通知區域圖示右下角;多個狀態同時滿足時,依紅 > 綠 > 黃 > 藍的優先順序僅顯示一個圓點。選擇「不顯示」可停用該顏色。", + "notify.trayTooltip": "顯示圖示懸浮名稱", + "notify.trayTooltipDesc": "滑鼠停留在通知區域圖示上時顯示「Boss Key」;關閉後不顯示任何文字,更為隱密。", + "options.generalCard": "一般", "options.muteAfterHide": "隱藏視窗後靜音", "options.muteAfterHideDesc": "隱藏時將目標程序靜音,復原顯示時自動取消靜音。", diff --git a/crates/common/src/config.rs b/crates/common/src/config.rs index b97659e..1ad23f5 100644 --- a/crates/common/src/config.rs +++ b/crates/common/src/config.rs @@ -212,6 +212,79 @@ impl MouseSetting { } } +/// 托盘角标可绑定的状态源取值(空串 = 不显示该颜色)。 +pub const TRAY_STATUS_HIDDEN: &str = "hidden"; +pub const TRAY_STATUS_AUTO_HIDE: &str = "auto_hide"; +pub const TRAY_STATUS_HIDE_CURRENT: &str = "hide_current"; +pub const TRAY_STATUS_FREEZE: &str = "freeze"; +pub const TRAY_STATUS_ELEVATED: &str = "elevated"; +pub const TRAY_STATUS_MONITOR_PAUSED: &str = "monitor_paused"; +/// 全部合法的非空状态源。 +pub const TRAY_STATUSES: [&str; 6] = [ + TRAY_STATUS_HIDDEN, + TRAY_STATUS_AUTO_HIDE, + TRAY_STATUS_HIDE_CURRENT, + TRAY_STATUS_FREEZE, + TRAY_STATUS_ELEVATED, + TRAY_STATUS_MONITOR_PAUSED, +]; + +fn default_badge_red() -> String { + TRAY_STATUS_HIDDEN.to_string() +} +fn default_badge_green() -> String { + TRAY_STATUS_AUTO_HIDE.to_string() +} +fn default_badge_yellow() -> String { + TRAY_STATUS_HIDE_CURRENT.to_string() +} +fn default_badge_blue() -> String { + TRAY_STATUS_FREEZE.to_string() +} + +/// 托盘图标状态角标:四种颜色各自绑定一个状态源。 +/// +/// 多个绑定状态同时活跃时按 **红 > 绿 > 黄 > 蓝** 的优先级只显示一个圆点; +/// 置空(`""`)表示该颜色不显示。 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TrayBadges { + #[serde(default = "default_badge_red")] + pub red: String, + #[serde(default = "default_badge_green")] + pub green: String, + #[serde(default = "default_badge_yellow")] + pub yellow: String, + #[serde(default = "default_badge_blue")] + pub blue: String, +} + +impl Default for TrayBadges { + fn default() -> Self { + Self { + red: default_badge_red(), + green: default_badge_green(), + yellow: default_badge_yellow(), + blue: default_badge_blue(), + } + } +} + +impl TrayBadges { + /// 未知状态源一律归一为置空(不显示),避免手改配置后角标行为不可预测。幂等。 + pub fn normalize(&mut self) { + for v in [ + &mut self.red, + &mut self.green, + &mut self.yellow, + &mut self.blue, + ] { + if !v.is_empty() && !TRAY_STATUSES.contains(&v.as_str()) { + v.clear(); + } + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Setting { #[serde(default = "default_true")] @@ -224,6 +297,12 @@ pub struct Setting { pub click_to_hide: bool, #[serde(default)] pub hide_icon_after_hide: bool, + /// 托盘图标状态角标的颜色绑定,见 [`TrayBadges`]。 + #[serde(default)] + pub tray_badges: TrayBadges, + /// 是否显示托盘图标的悬浮名称(Boss Key);关闭后悬停不显示任何文字。 + #[serde(default = "default_true")] + pub tray_show_tooltip: bool, #[serde(default)] pub freeze_after_hide: bool, #[serde(default)] @@ -281,6 +360,8 @@ impl Default for Setting { hide_current: true, click_to_hide: true, hide_icon_after_hide: false, + tray_badges: TrayBadges::default(), + tray_show_tooltip: true, freeze_after_hide: false, enhanced_freeze: false, freeze_whole_tree: false, @@ -315,6 +396,7 @@ impl Setting { self.middle_button_hide = false; self.side_button1_hide = false; self.side_button2_hide = false; + self.tray_badges.normalize(); self.mouse.normalize(); self.language = crate::i18n::normalize_pref(&self.language); } @@ -672,6 +754,57 @@ mod tests { assert!(!c.setting.autostart_admin, "自启默认普通权限"); } + #[test] + fn tray_badges_default_bindings() { + let d = TrayBadges::default(); + assert_eq!( + d.red, TRAY_STATUS_HIDDEN, + "红色默认绑定「存在隐藏中的窗口」" + ); + assert_eq!( + d.green, TRAY_STATUS_AUTO_HIDE, + "绿色默认绑定「启用了自动隐藏」" + ); + assert_eq!( + d.yellow, TRAY_STATUS_HIDE_CURRENT, + "黄色默认绑定「同时隐藏当前窗口」" + ); + assert_eq!(d.blue, TRAY_STATUS_FREEZE, "蓝色默认绑定「启用了进程冻结」"); + assert!(Setting::default().tray_show_tooltip, "悬浮名称默认显示"); + } + + #[test] + fn tray_badges_round_trip_including_empty() { + let c = Config::from_json( + r#"{"setting": {"tray_badges": {"red": "", "green": "freeze"}, "tray_show_tooltip": false}}"#, + ) + .unwrap(); + assert_eq!(c.setting.tray_badges.red, "", "置空表示不显示该颜色"); + assert_eq!(c.setting.tray_badges.green, TRAY_STATUS_FREEZE); + assert_eq!( + c.setting.tray_badges.yellow, TRAY_STATUS_HIDE_CURRENT, + "缺失的颜色用默认绑定" + ); + assert!(!c.setting.tray_show_tooltip); + let back = Config::from_json(&c.to_json().unwrap()).unwrap(); + assert_eq!( + back.setting.tray_badges, c.setting.tray_badges, + "写回后应保留" + ); + assert!(!back.setting.tray_show_tooltip, "写回后应保留"); + } + + #[test] + fn tray_badges_unknown_status_normalizes_to_empty() { + let c = Config::from_json(r#"{"setting": {"tray_badges": {"red": "no_such_status"}}}"#) + .unwrap(); + assert_eq!(c.setting.tray_badges.red, "", "未知状态源应归一为置空"); + assert_eq!( + c.setting.tray_badges.blue, TRAY_STATUS_FREEZE, + "其余颜色不受影响" + ); + } + #[test] fn autostart_admin_round_trips() { assert!(!Setting::default().autostart_admin, "默认关(普通权限)"); diff --git a/crates/common/src/ipc.rs b/crates/common/src/ipc.rs index 68d8d9c..3b58667 100644 --- a/crates/common/src/ipc.rs +++ b/crates/common/src/ipc.rs @@ -41,10 +41,13 @@ pub enum Response { elevated: bool, }, /// `monitoring`:核心是否正在监听热键与鼠标(被 `SetHotkeys` 停用时为 false)。 + /// `auto_hide_enabled`:自动隐藏当前是否启用(托盘菜单与设置界面均可切换,须回传对齐)。 Status { hidden: bool, elevated: bool, monitoring: bool, + #[serde(default)] + auto_hide_enabled: bool, }, Error { message: String, @@ -198,11 +201,13 @@ mod tests { hidden: true, elevated: false, monitoring: true, + auto_hide_enabled: false, }, Response::Status { hidden: false, elevated: true, monitoring: false, + auto_hide_enabled: true, }, Response::Error { message: "出错了".to_string(), diff --git a/crates/core/src/agent.rs b/crates/core/src/agent.rs index 211c201..7b379df 100644 --- a/crates/core/src/agent.rs +++ b/crates/core/src/agent.rs @@ -32,6 +32,7 @@ use crate::keyboard_hook::{self, KeyboardHook, WM_KEY_TRIGGER}; use crate::mouse_hook::{self, MouseHook, TRIGGER_CORNER, WM_MOUSE_TRIGGER}; use crate::platform::win32::WindowsWindowManager; use crate::tray::TrayIcon; +use crate::tray_badge::TrayIconSet; use crate::util::append_menu_item; use crate::{idle, ipc_server, logging, recovery}; @@ -50,6 +51,7 @@ const MENU_QUIT: usize = 1003; const MENU_AUTOSTART: usize = 1004; const MENU_RESTORE: usize = 1005; const MENU_ABOUT: usize = 1006; +const MENU_AUTO_HIDE: usize = 1007; const AUTO_QUIT_TIMER_ID: usize = 10; const AUTO_HIDE_TIMER_ID: usize = 11; @@ -85,6 +87,8 @@ struct AgentState { recovery_path: PathBuf, controller: HideController, tray: Option, + /// 托盘图标的角标变体缓存;未启用托盘时为 None。 + tray_icons: Option, ipc_rx: Receiver<(Command, Sender)>, mouse_hook: Option, float_window: Option, @@ -239,20 +243,44 @@ impl AgentState { } else { self.float_window = None; } + + self.update_tray_icon(); } fn sync_tray(&mut self) { - if !self.config.setting.hide_icon_after_hide { - return; - } let hidden = self.controller.is_hidden(); - if let Some(tray) = &mut self.tray { + if self.config.setting.hide_icon_after_hide + && let Some(tray) = &mut self.tray + { if hidden { tray.hide(); } else { tray.show(); } } + self.update_tray_icon(); + } + + /// 让托盘图标的状态角标与悬浮提示对齐当前配置与运行状态。均未变化时开销为零。 + fn update_tray_icon(&mut self) { + let (Some(tray), Some(icons)) = (&mut self.tray, &mut self.tray_icons) else { + return; + }; + let status = crate::tray_badge::TrayStatus { + hidden: self.controller.is_hidden(), + auto_hide: self.config.setting.auto_hide_enabled, + hide_current: self.config.setting.hide_current, + freeze: self.config.setting.freeze_after_hide, + elevated: crate::elevation::is_elevated(), + monitor_paused: !self.monitoring, + }; + let badge = crate::tray_badge::active_badge(&self.config.setting.tray_badges, &status); + tray.set_icon(icons.icon(badge)); + tray.set_tip(if self.config.setting.tray_show_tooltip { + APP_NAME + } else { + "" + }); } /// 隐藏状态落盘:崩溃后下次启动据此找回窗口。快照为空时清除文件。 @@ -398,6 +426,7 @@ impl AgentState { hidden: self.controller.is_hidden(), elevated: crate::elevation::is_elevated(), monitoring: self.monitoring, + auto_hide_enabled: self.config.setting.auto_hide_enabled, }, false, ), @@ -531,6 +560,25 @@ fn set_autostart(enabled: bool, admin: bool) -> Response { } } +/// 托盘菜单切换自动隐藏:翻转配置并落盘,与在设置界面切换等效。 +/// 设置界面经 2 秒一次的状态轮询回读该值,保持两侧一致。 +fn toggle_auto_hide(state: &mut AgentState, hwnd: HWND) { + let enabled = !state.config.setting.auto_hide_enabled; + state.config.setting.auto_hide_enabled = enabled; + logging::info(if enabled { + "托盘菜单:已启用自动隐藏" + } else { + "托盘菜单:已暂停自动隐藏" + }); + if let Err(e) = state.config.save(&state.config_path) { + logging::warn(&format!( + "自动隐藏开关写入配置失败,核心重启后将丢失本次切换: {e}" + )); + } + // 重新武装/停掉空闲检测定时器,并刷新托盘角标。 + state.refresh_runtime(hwnd); +} + fn toggle_autostart(state: &AgentState) { let Ok(auto) = crate::autostart::Autostart::standard() else { return; @@ -567,7 +615,7 @@ fn state_mut<'a>(hwnd: HWND) -> Option<&'a mut AgentState> { } } -fn show_tray_menu(hwnd: HWND, hidden: bool) -> bool { +fn show_tray_menu(hwnd: HWND, hidden: bool, auto_hide_on: bool) -> bool { let autostart_on = crate::autostart::Autostart::standard() .map(|a| a.status().is_some()) .unwrap_or(false); @@ -580,15 +628,28 @@ fn show_tray_menu(hwnd: HWND, hidden: bool) -> bool { } else { Msg::MenuHideWindows }; - let autostart_flags = if autostart_on { - MF_STRING | MF_CHECKED - } else { - MF_STRING + let checked = |on: bool| { + if on { + MF_STRING | MF_CHECKED + } else { + MF_STRING + } }; append_menu_item(menu, MF_STRING, MENU_SETTINGS, Msg::MenuSettings); append_menu_item(menu, MF_STRING, MENU_TOGGLE, toggle_label); append_menu_item(menu, MF_STRING, MENU_RESTORE, Msg::MenuRestoreTool); - append_menu_item(menu, autostart_flags, MENU_AUTOSTART, Msg::MenuAutostart); + append_menu_item( + menu, + checked(auto_hide_on), + MENU_AUTO_HIDE, + Msg::MenuAutoHide, + ); + append_menu_item( + menu, + checked(autostart_on), + MENU_AUTOSTART, + Msg::MenuAutostart, + ); append_menu_item(menu, MF_STRING, MENU_ABOUT, Msg::MenuAbout); let _ = AppendMenuW(menu, MF_SEPARATOR, 0, PCWSTR::null()); append_menu_item(menu, MF_STRING, MENU_QUIT, Msg::MenuQuit); @@ -731,7 +792,11 @@ unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: } } WM_RBUTTONUP => { - show_tray_menu(hwnd, state.controller.is_hidden()); + show_tray_menu( + hwnd, + state.controller.is_hidden(), + state.config.setting.auto_hide_enabled, + ); } _ => {} } @@ -759,6 +824,7 @@ unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: MENU_RESTORE => launch_settings(state, Some(ARG_RESTORE)), MENU_ABOUT => launch_settings(state, Some(ARG_ABOUT)), MENU_TOGGLE => state.apply_toggle(), + MENU_AUTO_HIDE => toggle_auto_hide(state, hwnd), MENU_AUTOSTART => toggle_autostart(state), MENU_QUIT => quit(state, hwnd), _ => {} @@ -971,12 +1037,15 @@ pub fn run(options: AgentOptions) { .config_path .with_file_name(recovery::RECOVERY_FILE_NAME); + let tray_icons = tray.as_ref().map(|_| TrayIconSet::new()); + let mut state = Box::new(AgentState { config, config_path: options.config_path.clone(), recovery_path, controller: HideController::new(WindowsWindowManager, WinEffects::new(exe_dir)), tray, + tray_icons, ipc_rx, mouse_hook: None, float_window: None, diff --git a/crates/core/src/i18n.rs b/crates/core/src/i18n.rs index a1d4710..db163b2 100644 --- a/crates/core/src/i18n.rs +++ b/crates/core/src/i18n.rs @@ -14,6 +14,7 @@ pub enum Msg { MenuShowWindows, MenuHideWindows, MenuRestoreTool, + MenuAutoHide, MenuAutostart, MenuAbout, MenuQuit, @@ -59,6 +60,7 @@ impl Msg { Msg::MenuShowWindows => "显示窗口", Msg::MenuHideWindows => "隐藏窗口", Msg::MenuRestoreTool => "窗口恢复工具", + Msg::MenuAutoHide => "自动隐藏", Msg::MenuAutostart => "开机自启", Msg::MenuAbout => "关于", Msg::MenuQuit => "退出", @@ -95,6 +97,7 @@ impl Msg { Msg::MenuShowWindows => "Show Windows", Msg::MenuHideWindows => "Hide Windows", Msg::MenuRestoreTool => "Window Recovery Tool", + Msg::MenuAutoHide => "Auto Hide", Msg::MenuAutostart => "Start with Windows", Msg::MenuAbout => "About", Msg::MenuQuit => "Exit", @@ -131,6 +134,7 @@ impl Msg { Msg::MenuShowWindows => "顯示視窗", Msg::MenuHideWindows => "隱藏視窗", Msg::MenuRestoreTool => "視窗復原工具", + Msg::MenuAutoHide => "自動隱藏", Msg::MenuAutostart => "開機自動啟動", Msg::MenuAbout => "關於", Msg::MenuQuit => "結束", @@ -210,11 +214,12 @@ mod tests { use super::*; /// 全部文案键;新增 Msg 变体后必须同步登记,否则跨语言校验会漏掉它。 - const ALL_MSGS: [Msg; 31] = [ + const ALL_MSGS: [Msg; 32] = [ Msg::MenuSettings, Msg::MenuShowWindows, Msg::MenuHideWindows, Msg::MenuRestoreTool, + Msg::MenuAutoHide, Msg::MenuAutostart, Msg::MenuAbout, Msg::MenuQuit, diff --git a/crates/core/src/icon.rs b/crates/core/src/icon.rs index 5499932..2447045 100644 --- a/crates/core/src/icon.rs +++ b/crates/core/src/icon.rs @@ -56,7 +56,7 @@ pub fn extract_icon_rgba(path: &str) -> Option { rgba } -fn hicon_to_rgba(hicon: HICON) -> Option { +pub(crate) fn hicon_to_rgba(hicon: HICON) -> Option { unsafe { let mut icon_info = ICONINFO::default(); GetIconInfo(hicon, &mut icon_info).ok()?; diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 49efa8c..1e10722 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -20,5 +20,6 @@ pub mod recovery; pub mod shell; pub mod single_instance; pub mod tray; +pub mod tray_badge; mod util; diff --git a/crates/core/src/tray.rs b/crates/core/src/tray.rs index 2a8f307..aa334fc 100644 --- a/crates/core/src/tray.rs +++ b/crates/core/src/tray.rs @@ -204,6 +204,35 @@ impl TrayIcon { self.visible } + /// 替换托盘图标(状态角标变化时调用)。已挂载时立即 NIM_MODIFY 生效, + /// 未挂载时仅记下句柄,待补挂时一并带上。图标未变化时不重复提交。 + pub fn set_icon(&mut self, icon: HICON) { + if self.icon == icon { + return; + } + self.icon = icon; + self.modify_if_visible(); + } + + /// 替换悬浮提示文字(空串 = 悬停不显示任何文字)。文字未变化时不重复提交。 + pub fn set_tip(&mut self, tip: &str) { + if self.tip == tip { + return; + } + self.tip = tip.to_string(); + self.modify_if_visible(); + } + + fn modify_if_visible(&self) { + if !self.visible { + return; + } + let data = self.base_data(); + unsafe { + let _ = Shell_NotifyIconW(NIM_MODIFY, &data); + } + } + pub fn balloon(&self, title: &str, message: &str) { if !self.visible { return; diff --git a/crates/core/src/tray_badge.rs b/crates/core/src/tray_badge.rs new file mode 100644 index 0000000..8d1c933 --- /dev/null +++ b/crates/core/src/tray_badge.rs @@ -0,0 +1,428 @@ +//! 托盘图标状态角标:在基础图标右下角叠加一个彩色圆点,反映核心当前状态。 +//! +//! 四种颜色各自绑定一个状态源(见 `TrayBadges`),多个绑定状态同时活跃时 +//! 按 **红 > 绿 > 黄 > 蓝** 的优先级只显示一个圆点;置空的颜色不参与。 + +use std::collections::HashMap; + +use bosskey_common::config::{ + TRAY_STATUS_AUTO_HIDE, TRAY_STATUS_ELEVATED, TRAY_STATUS_FREEZE, TRAY_STATUS_HIDDEN, + TRAY_STATUS_HIDE_CURRENT, TRAY_STATUS_MONITOR_PAUSED, TrayBadges, +}; +use windows::Win32::Graphics::Gdi::{ + BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateBitmap, CreateDIBSection, DIB_RGB_COLORS, + DeleteObject, GetDC, ReleaseDC, +}; +use windows::Win32::UI::WindowsAndMessaging::{CreateIconIndirect, HICON, ICONINFO}; + +use crate::icon::IconRgba; + +/// 角标描边:白色,用于在深浅任务栏背景上都能衬出圆点边界。 +const BADGE_RING: [u8; 4] = [0xFF, 0xFF, 0xFF, 0xFF]; + +/// 角标颜色,declaration 顺序即显示优先级(红最高)。 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BadgeColor { + Red, + Green, + Yellow, + Blue, +} + +impl BadgeColor { + const fn rgba(self) -> [u8; 4] { + match self { + BadgeColor::Red => [0xF4, 0x43, 0x36, 0xFF], + BadgeColor::Green => [0x4C, 0xAF, 0x50, 0xFF], + BadgeColor::Yellow => [0xFF, 0xC1, 0x07, 0xFF], + BadgeColor::Blue => [0x21, 0x96, 0xF3, 0xFF], + } + } +} + +/// 核心当前的状态快照,供角标绑定判断。 +#[derive(Debug, Clone, Copy, Default)] +pub struct TrayStatus { + /// 当前有窗口被隐藏。 + pub hidden: bool, + /// 空闲自动隐藏已启用。 + pub auto_hide: bool, + /// 「同时隐藏当前活动窗口」已启用。 + pub hide_current: bool, + /// 「隐藏窗口时冻结进程」已启用。 + pub freeze: bool, + /// 核心正以管理员身份运行。 + pub elevated: bool, + /// 热键与鼠标监控已被临时停用(见 `Command::SetHotkeys`)。 + pub monitor_paused: bool, +} + +/// 某个状态源当前是否活跃;空串与未知取值一律视为不活跃。 +fn status_active(key: &str, s: &TrayStatus) -> bool { + match key { + TRAY_STATUS_HIDDEN => s.hidden, + TRAY_STATUS_AUTO_HIDE => s.auto_hide, + TRAY_STATUS_HIDE_CURRENT => s.hide_current, + TRAY_STATUS_FREEZE => s.freeze, + TRAY_STATUS_ELEVATED => s.elevated, + TRAY_STATUS_MONITOR_PAUSED => s.monitor_paused, + _ => false, + } +} + +/// 按红 > 绿 > 黄 > 蓝的优先级选出当前应显示的角标颜色;都不活跃时不显示。 +pub fn active_badge(badges: &TrayBadges, status: &TrayStatus) -> Option { + [ + (BadgeColor::Red, badges.red.as_str()), + (BadgeColor::Green, badges.green.as_str()), + (BadgeColor::Yellow, badges.yellow.as_str()), + (BadgeColor::Blue, badges.blue.as_str()), + ] + .into_iter() + .find(|(_, key)| status_active(key, status)) + .map(|(color, _)| color) +} + +/// 在顶到底 RGBA 像素的右下角叠加一个带白色描边的实心圆点。 +/// +/// 圆点直径约为图标边长的 44%,紧贴右下角;边缘做 1px 线性过渡抗锯齿。 +/// `pixels` 长度必须等于 `width*height*4`,否则不做任何修改。 +fn overlay_badge(pixels: &mut [u8], width: u32, height: u32, color: [u8; 4]) { + if width == 0 || height == 0 || pixels.len() != (width * height * 4) as usize { + return; + } + let size = width.min(height) as f32; + let radius = (size * 0.22).max(2.5); + let ring = (size * 0.05).max(1.0); + let cx = width as f32 - radius - ring - 0.5; + let cy = height as f32 - radius - ring - 0.5; + + for y in 0..height { + for x in 0..width { + let dx = x as f32 - cx; + let dy = y as f32 - cy; + let dist = (dx * dx + dy * dy).sqrt(); + // 覆盖率:圆内 1,圆外 0,边界 1px 内线性过渡。 + let dot = (radius + 0.5 - dist).clamp(0.0, 1.0); + let outline = (radius + ring + 0.5 - dist).clamp(0.0, 1.0) - dot; + if dot <= 0.0 && outline <= 0.0 { + continue; + } + let i = ((y * width + x) * 4) as usize; + let px = &mut pixels[i..i + 4]; + blend(px, BADGE_RING, outline); + blend(px, color, dot); + } + } +} + +/// 把 `src` 以 `coverage`(0..=1)的强度 alpha 混合到 `dst`(均为直通 alpha 的 RGBA)。 +fn blend(dst: &mut [u8], src: [u8; 4], coverage: f32) { + if coverage <= 0.0 { + return; + } + let a = coverage * (src[3] as f32 / 255.0); + for c in 0..3 { + dst[c] = (src[c] as f32 * a + dst[c] as f32 * (1.0 - a)).round() as u8; + } + dst[3] = ((255.0 * a) + dst[3] as f32 * (1.0 - a)).round() as u8; +} + +/// 把指定颜色的圆点叠加到基础图标像素上。 +fn compose(base: &IconRgba, color: BadgeColor) -> IconRgba { + let mut out = base.clone(); + overlay_badge(&mut out.pixels, out.width, out.height, color.rgba()); + out +} + +/// 顶到底 RGBA 像素 → HICON(32 位 ARGB,直通 alpha)。 +fn rgba_to_hicon(icon: &IconRgba) -> Option { + if icon.width == 0 + || icon.height == 0 + || icon.pixels.len() != (icon.width * icon.height * 4) as usize + { + return None; + } + unsafe { + let bmi = BITMAPINFO { + bmiHeader: BITMAPINFOHEADER { + biSize: std::mem::size_of::() as u32, + biWidth: icon.width as i32, + biHeight: -(icon.height as i32), // 负值 = 顶到底行序 + biPlanes: 1, + biBitCount: 32, + biCompression: BI_RGB.0, + ..Default::default() + }, + ..Default::default() + }; + let mut bits: *mut std::ffi::c_void = std::ptr::null_mut(); + let hdc = GetDC(None); + let color = CreateDIBSection(Some(hdc), &bmi, DIB_RGB_COLORS, &mut bits, None, 0); + ReleaseDC(None, hdc); + let color = color.ok()?; + if bits.is_null() { + let _ = DeleteObject(color.into()); + return None; + } + // RGBA → BGRA。 + let dst = std::slice::from_raw_parts_mut(bits as *mut u8, icon.pixels.len()); + for (d, s) in dst.chunks_exact_mut(4).zip(icon.pixels.chunks_exact(4)) { + d[0] = s[2]; + d[1] = s[1]; + d[2] = s[0]; + d[3] = s[3]; + } + let mask = CreateBitmap(icon.width as i32, icon.height as i32, 1, 1, None); + let info = ICONINFO { + fIcon: true.into(), + hbmMask: mask, + hbmColor: color, + ..Default::default() + }; + let hicon = CreateIconIndirect(&info).ok(); + let _ = DeleteObject(color.into()); + if !mask.is_invalid() { + let _ = DeleteObject(mask.into()); + } + hicon + } +} + +/// 基础图标 + 各颜色角标的 HICON 缓存。 +/// +/// 颜色数量有限(4 种),生成后缓存复用;缓存的 HICON 随进程存活,不逐个销毁。 +pub struct TrayIconSet { + base: HICON, + base_rgba: Option, + variants: HashMap, +} + +impl TrayIconSet { + pub fn new() -> Self { + let base = crate::tray::load_app_icon(); + Self { + base, + base_rgba: crate::icon::hicon_to_rgba(base), + variants: HashMap::new(), + } + } + + /// 指定角标颜色的图标;无角标、像素提取或生成失败时回退基础图标。 + pub fn icon(&mut self, badge: Option) -> HICON { + let Some(color) = badge else { + return self.base; + }; + if let Some(icon) = self.variants.get(&color) { + return *icon; + } + let Some(base_rgba) = &self.base_rgba else { + return self.base; + }; + match rgba_to_hicon(&compose(base_rgba, color)) { + Some(icon) => { + self.variants.insert(color, icon); + icon + } + None => self.base, + } + } +} + +impl Default for TrayIconSet { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn all_bound() -> TrayBadges { + TrayBadges::default() + } + + #[test] + fn default_bindings_pick_expected_colors() { + let b = all_bound(); + let s = |st: TrayStatus| active_badge(&b, &st); + assert_eq!( + s(TrayStatus { + hidden: true, + ..Default::default() + }), + Some(BadgeColor::Red), + "存在隐藏窗口时应显示红色" + ); + assert_eq!( + s(TrayStatus { + auto_hide: true, + ..Default::default() + }), + Some(BadgeColor::Green), + "启用自动隐藏时应显示绿色" + ); + assert_eq!( + s(TrayStatus { + hide_current: true, + ..Default::default() + }), + Some(BadgeColor::Yellow), + "启用同时隐藏当前窗口时应显示黄色" + ); + assert_eq!( + s(TrayStatus { + freeze: true, + ..Default::default() + }), + Some(BadgeColor::Blue), + "启用进程冻结时应显示蓝色" + ); + assert_eq!(s(TrayStatus::default()), None, "无活跃状态时不显示角标"); + } + + #[test] + fn priority_red_over_green_over_yellow_over_blue() { + let b = all_bound(); + let all_on = TrayStatus { + hidden: true, + auto_hide: true, + hide_current: true, + freeze: true, + ..Default::default() + }; + assert_eq!( + active_badge(&b, &all_on), + Some(BadgeColor::Red), + "全部活跃时红色优先级最高" + ); + let no_hidden = TrayStatus { + hidden: false, + ..all_on + }; + assert_eq!( + active_badge(&b, &no_hidden), + Some(BadgeColor::Green), + "红色不活跃时轮到绿色" + ); + let only_low = TrayStatus { + hide_current: true, + freeze: true, + ..Default::default() + }; + assert_eq!( + active_badge(&b, &only_low), + Some(BadgeColor::Yellow), + "黄色优先于蓝色" + ); + } + + #[test] + fn empty_binding_skips_color_and_falls_through() { + // 红色置空:即使有隐藏窗口,也应轮到下一个活跃颜色。 + let b = TrayBadges { + red: String::new(), + ..TrayBadges::default() + }; + let st = TrayStatus { + hidden: true, + freeze: true, + ..Default::default() + }; + assert_eq!( + active_badge(&b, &st), + Some(BadgeColor::Blue), + "置空的颜色不显示,由更低优先级的活跃颜色顶上" + ); + } + + #[test] + fn extended_statuses_can_be_bound() { + let b = TrayBadges { + red: String::new(), + green: TRAY_STATUS_MONITOR_PAUSED.to_string(), + yellow: String::new(), + blue: TRAY_STATUS_ELEVATED.to_string(), + }; + let admin_only = TrayStatus { + elevated: true, + ..Default::default() + }; + assert_eq!( + active_badge(&b, &admin_only), + Some(BadgeColor::Blue), + "管理员身份可绑定为角标状态" + ); + let paused = TrayStatus { + elevated: true, + monitor_paused: true, + ..Default::default() + }; + assert_eq!( + active_badge(&b, &paused), + Some(BadgeColor::Green), + "监控暂停可绑定为角标状态,且绿色优先于蓝色" + ); + } + + #[test] + fn rebinding_a_color_changes_its_trigger() { + // 绿色改绑「存在隐藏窗口」:隐藏时显示绿点(红色已置空)。 + let b = TrayBadges { + red: String::new(), + green: bosskey_common::config::TRAY_STATUS_HIDDEN.to_string(), + ..TrayBadges::default() + }; + let st = TrayStatus { + hidden: true, + ..Default::default() + }; + assert_eq!(active_badge(&b, &st), Some(BadgeColor::Green)); + } + + fn blank(width: u32, height: u32) -> IconRgba { + IconRgba { + width, + height, + pixels: vec![0u8; (width * height * 4) as usize], + } + } + + fn pixel(icon: &IconRgba, x: u32, y: u32) -> [u8; 4] { + let i = ((y * icon.width + x) * 4) as usize; + icon.pixels[i..i + 4].try_into().unwrap() + } + + #[test] + fn badge_paints_bottom_right_dot_and_leaves_far_corner_untouched() { + let icon = compose(&blank(32, 32), BadgeColor::Red); + let px = pixel(&icon, 24, 24); + assert_eq!(&px[..3], &BadgeColor::Red.rgba()[..3], "圆心应为角标颜色"); + assert_eq!(px[3], 255, "圆心应完全不透明"); + assert_eq!(pixel(&icon, 2, 2), [0, 0, 0, 0], "对角像素应保持原样"); + } + + #[test] + fn badge_survives_small_icons() { + let icon = compose(&blank(16, 16), BadgeColor::Blue); + assert!( + icon.pixels.chunks_exact(4).any(|px| px[3] == 255), + "16×16 图标上也应画出角标" + ); + } + + #[test] + fn mismatched_buffer_is_left_untouched() { + let mut pixels = vec![0u8; 10]; + overlay_badge(&mut pixels, 32, 32, BadgeColor::Red.rgba()); + assert_eq!(pixels, vec![0u8; 10], "长度不符时不得越界或修改"); + } + + #[test] + fn compose_does_not_mutate_base() { + let base = blank(32, 32); + let _ = compose(&base, BadgeColor::Yellow); + assert!(base.pixels.iter().all(|b| *b == 0), "compose 不得修改原图"); + } +} diff --git a/crates/core/tests/agent_ipc.rs b/crates/core/tests/agent_ipc.rs index ed7ebd3..5eb1253 100644 --- a/crates/core/tests/agent_ipc.rs +++ b/crates/core/tests/agent_ipc.rs @@ -7,9 +7,10 @@ use bosskey_core::agent::{self, AgentOptions}; fn agent_answers_ipc_and_quits_cleanly() { let dir = tempfile::tempdir().unwrap(); let config_path = dir.path().join("config.json"); - bosskey_common::Config::default() - .save(&config_path) - .unwrap(); + let mut config = bosskey_common::Config::default(); + // 开启自动隐藏,验证 GetStatus 会如实回传该开关(供设置界面与托盘对齐)。 + config.setting.auto_hide_enabled = true; + config.save(&config_path).unwrap(); let pipe = r"\\.\pipe\bosskey_test_agent_e2e"; let options = AgentOptions { @@ -32,8 +33,15 @@ fn agent_answers_ipc_and_quits_cleanly() { let status = client.send(&Command::GetStatus).unwrap(); assert!( - matches!(status, Response::Status { hidden: false, .. }), - "合并状态应一次往返返回隐藏态与权限: {status:?}" + matches!( + status, + Response::Status { + hidden: false, + auto_hide_enabled: true, + .. + } + ), + "合并状态应一次往返返回隐藏态、权限与自动隐藏开关: {status:?}" ); let reload = client.send(&Command::ReloadConfig).unwrap(); diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index caeeadd..0031f49 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -84,7 +84,7 @@ export default defineConfig(async () => { { text: '热键与鼠标手势', link: '/guide/hotkeys' }, { text: '进程冻结', link: '/guide/freeze' }, { text: '其他选项', link: '/guide/options' }, - { text: '通知设置', link: '/guide/notifications' }, + { text: '提示设置', link: '/guide/notifications' }, { text: '开机自启', link: '/guide/autostart' }, ], }, @@ -192,7 +192,7 @@ export default defineConfig(async () => { { text: 'Hotkeys & mouse gestures', link: '/en/guide/hotkeys' }, { text: 'Process freezing', link: '/en/guide/freeze' }, { text: 'Other options', link: '/en/guide/options' }, - { text: 'Notifications', link: '/en/guide/notifications' }, + { text: 'Alerts', link: '/en/guide/notifications' }, { text: 'Start with Windows', link: '/en/guide/autostart' }, ], }, @@ -290,7 +290,7 @@ export default defineConfig(async () => { { text: '快速鍵與滑鼠手勢', link: '/zh-tw/guide/hotkeys' }, { text: '程序凍結', link: '/zh-tw/guide/freeze' }, { text: '其他選項', link: '/zh-tw/guide/options' }, - { text: '通知設定', link: '/zh-tw/guide/notifications' }, + { text: '提示設定', link: '/zh-tw/guide/notifications' }, { text: '開機自動啟動', link: '/zh-tw/guide/autostart' }, ], }, diff --git a/docs/dev/config-reference.md b/docs/dev/config-reference.md index 90df20b..e733bdb 100644 --- a/docs/dev/config-reference.md +++ b/docs/dev/config-reference.md @@ -49,6 +49,8 @@ Boss Key 的配置保存在与可执行文件**同目录**的 `config.json` 中 | `hide_current` | bool | `true` | [同时隐藏当前活动窗口](/guide/options#同时隐藏当前活动窗口) | | `click_to_hide` | bool | `true` | [单击托盘切换隐藏](/guide/options#单击托盘图标切换隐藏) | | `hide_icon_after_hide` | bool | `false` | [隐藏后同时隐藏托盘图标](/guide/options#隐藏后同时隐藏托盘图标) | +| `tray_badges` | object | 见下 | [图标状态提示](/guide/notifications#图标状态提示) | +| `tray_show_tooltip` | bool | `true` | [显示图标悬浮名称](/guide/notifications#显示图标悬浮名称) | | `freeze_after_hide` | bool | `false` | [进程冻结总开关](/guide/freeze#隐藏窗口时冻结进程) | | `enhanced_freeze` | bool | `false` | [增强冻结](/guide/freeze#使用增强冻结) | | `freeze_whole_tree` | bool | `false` | [冻结完整进程](/guide/freeze#冻结完整进程) | @@ -85,6 +87,19 @@ Boss Key 的配置保存在与可执行文件**同目录**的 `config.json` 中 全新安装默认开启**中键单击**(`middle.enabled = true`,`clicks = 1`),其余四颗关闭。配置文件缺 `mouse` 一节的老配置读进来则**全关**。 ::: +### `setting.tray_badges` + +[图标状态提示](/guide/notifications#图标状态提示):四种颜色的圆点角标各自绑定一个状态源,多个状态同时活跃时按**红 > 绿 > 黄 > 蓝**的优先级只显示一个圆点。 + +| 字段 | 默认 | 默认含义 | +| --- | --- | --- | +| `red` | `"hidden"` | 存在隐藏中的窗口 | +| `green` | `"auto_hide"` | 启用了自动隐藏 | +| `yellow` | `"hide_current"` | 启用了同时隐藏当前窗口 | +| `blue` | `"freeze"` | 启用了进程冻结 | + +每项取值:`hidden`(存在隐藏中的窗口)|`auto_hide`(启用了自动隐藏)|`hide_current`(启用了同时隐藏当前窗口)|`freeze`(启用了进程冻结)|`elevated`(以管理员身份运行)|`monitor_paused`(热键监控已暂停)|`""`(置空 = 不显示该颜色);未知取值读取时归一为置空。 + ## `notifications` | 字段 | 默认 | 说明 | diff --git a/docs/dev/frontend.md b/docs/dev/frontend.md index af2938a..619435e 100644 --- a/docs/dev/frontend.md +++ b/docs/dev/frontend.md @@ -43,7 +43,7 @@ apps/config/ui/ | --- | --- | --- | | 窗口绑定 | `BindingPanel` | 窗口列表 + 窗口/进程规则 | | 热键与鼠标 | `HotkeysPanel` | 键盘热键、鼠标连击、四角、空闲自动隐藏 | -| 通知设置 | `NotificationsPanel` | 逐事件通知开关 | +| 提示设置 | `NotificationsPanel` | 逐事件通知开关与图标状态角标 | | 其他选项 | `OptionsPanel` | 静音/暂停/冻结/权限/日志/工具 | | 关于与反馈 | `AboutPanel` | 版本、更新、公告、反馈 | diff --git a/docs/en/dev/config-reference.md b/docs/en/dev/config-reference.md index 0d073a5..76f8b69 100644 --- a/docs/en/dev/config-reference.md +++ b/docs/en/dev/config-reference.md @@ -49,6 +49,8 @@ The settings window reads and writes the configuration automatically. This page | `hide_current` | bool | `true` | [Also hide the active window](/en/guide/options) | | `click_to_hide` | bool | `true` | [Toggle hiding by clicking the tray icon](/en/guide/options) | | `hide_icon_after_hide` | bool | `false` | [Also hide the tray icon](/en/guide/options) | +| `tray_badges` | object | See below | [Tray icon status](/en/guide/notifications#tray-icon-status) | +| `tray_show_tooltip` | bool | `true` | [Tray icon tooltip](/en/guide/notifications#tray-icon-tooltip) | | `freeze_after_hide` | bool | `false` | [Freezing master switch](/en/guide/freeze) | | `enhanced_freeze` | bool | `false` | [Enhanced freezing](/en/guide/freeze) | | `freeze_whole_tree` | bool | `false` | [Freeze the whole process tree](/en/guide/freeze) | @@ -85,6 +87,19 @@ Each button is a `MouseButton`: `{ enabled: bool, clicks: 1..=3, modifiers: stri A fresh installation enables **a middle-button single click** (`middle.enabled = true`, `clicks = 1`) and leaves the other four off. An old configuration without a `mouse` section reads as **all off**. ::: +### `setting.tray_badges` + +[Tray icon status](/en/guide/notifications#tray-icon-status): each of the four colored dot badges is bound to a state source; when several bound states are active at once, only one dot is shown, in **red > green > yellow > blue** priority order. + +| Field | Default | Default meaning | +| --- | --- | --- | +| `red` | `"hidden"` | Windows are hidden | +| `green` | `"auto_hide"` | Auto hide is enabled | +| `yellow` | `"hide_current"` | Also-hide-active-window is enabled | +| `blue` | `"freeze"` | Process freezing is enabled | + +Each field accepts `hidden` (windows are hidden) \| `auto_hide` (auto hide is enabled) \| `hide_current` (also-hide-active-window is enabled) \| `freeze` (process freezing is enabled) \| `elevated` (running as administrator) \| `monitor_paused` (hotkey monitoring is paused) \| `""` (empty = do not show that color); unknown values are normalised to empty on read. + ## `notifications` | Field | Default | Description | diff --git a/docs/en/dev/frontend.md b/docs/en/dev/frontend.md index c7b1e2b..77597af 100644 --- a/docs/en/dev/frontend.md +++ b/docs/en/dev/frontend.md @@ -43,7 +43,7 @@ The settings window is organised as tabs, one panel component each: | --- | --- | --- | | Windows | `BindingPanel` | Window list + window/process rules | | Hotkeys & Mouse | `HotkeysPanel` | Keyboard hotkeys, mouse clicks, corners, auto-hide on idle | -| Notifications | `NotificationsPanel` | Per-event notification switches | +| Alerts | `NotificationsPanel` | Per-event notification switches and tray icon badges | | Options | `OptionsPanel` | Muting/pausing/freezing/privileges/logs/tools | | About & Feedback | `AboutPanel` | Version, updates, announcements, feedback | diff --git a/docs/en/guide/getting-started.md b/docs/en/guide/getting-started.md index a87b3a1..e44c24e 100644 --- a/docs/en/guide/getting-started.md +++ b/docs/en/guide/getting-started.md @@ -16,7 +16,7 @@ The **first time** you open Boss Key after installing or updating, the **setting | --- | --- | --- | | **Windows** | Choose the windows / processes to hide | [Binding windows & processes](/en/guide/binding) | | **Hotkeys & Mouse** | Keyboard hotkeys, mouse clicks, corner gestures, auto-hide on idle | [Hotkeys & mouse gestures](/en/guide/hotkeys) | -| **Notifications** | Per-event control of tray notifications | [Notifications](/en/guide/notifications) | +| **Alerts** | Notifications and tray icon status badges | [Alerts](/en/guide/notifications) | | **Options** | Muting, pause key, freezing, privileges, logs, recovery tool, and more | [Other options](/en/guide/options) | | **About & Feedback** | Version information, update checks, announcements, feedback | [Updates & feedback](/en/guide/update) | @@ -72,6 +72,7 @@ Day to day, **right-click the tray icon** to open the menu. It offers: - **Settings**: open the settings window. - **Hide Windows / Show Windows**: quickly hide or show the bound windows. - **Window Recovery Tool**: open the settings window on the window recovery tool — see [Window recovery & crash self-healing](/en/guide/recovery). +- **Auto Hide**: pause / resume [auto-hide on idle](/en/guide/hotkeys#auto-hide-when-idle); a check mark means it is currently enabled. - **Start with Windows**: toggle starting automatically when you sign in. - **Exit**: end the core process. diff --git a/docs/en/guide/hotkeys.md b/docs/en/guide/hotkeys.md index e3d9290..a8b8f06 100644 --- a/docs/en/guide/hotkeys.md +++ b/docs/en/guide/hotkeys.md @@ -92,6 +92,7 @@ Corner hiding relies on a low-level mouse hook (`WH_MOUSE_LL`), installed only w With **Enable auto-hide** on, Boss Key hides the bound windows automatically once the keyboard and mouse have been **idle** for the configured time. - **Idle time**: the period without input (in minutes) that triggers it. Default **5 minutes**. +- **Quick toggle from the tray menu**: right-click the tray icon and click **Auto Hide** to pause / resume auto-hide without opening the settings window. A check mark means it is currently enabled, and the change stays in sync with the settings window. The tray icon can also show this state as a badge — see [Tray icon status](/en/guide/notifications#tray-icon-status). ## Summary diff --git a/docs/en/guide/notifications.md b/docs/en/guide/notifications.md index aadd82e..7371368 100644 --- a/docs/en/guide/notifications.md +++ b/docs/en/guide/notifications.md @@ -1,10 +1,10 @@ --- -title: Notifications +title: Alerts --- -# Notifications +# Alerts -Boss Key shows **tray notifications** when certain events happen. The **Notifications** tab of the settings window lets you control which ones appear, so you stay informed without being interrupted. +Boss Key reflects the core's state through two kinds of hints: **tray notifications** and **tray icon badges**. The **Alerts** tab of the settings window lets you control which notifications appear and which states the icon badge shows, so you stay informed without being interrupted. ## Controllable events @@ -16,11 +16,35 @@ Boss Key shows **tray notifications** when certain events happen. The **Notifica | **Windows hidden** | Shown each time windows are hidden | Off | | **Windows shown** | Shown each time windows are restored | Off | +## Tray icon status + +The tray icon can overlay **a colored dot badge in its bottom-right corner** that reflects the core's current state. Each of the four colors is bound to a state source; the defaults are: + +| Color | Default binding | +| --- | --- | +| Red | Windows are hidden | +| Green | Auto hide is enabled | +| Yellow | Also-hide-active-window is enabled | +| Blue | Process freezing is enabled | + +- Each color can be rebound to another state (besides the four above, **Running as administrator** and **Hotkey monitoring is paused** are also available), or set to "Do not show" to disable it. +- When several bound states are active at once, only one dot is shown, in **red > green > yellow > blue** priority order. + +::: warning Discretion note +Badges — especially "Windows are hidden" — also reveal the core's state to onlookers. Disable the corresponding color if discretion matters more to you. +::: + +## Tray icon tooltip + +Hovering over the tray icon shows "Boss Key" by default. Turn this option off to show no text at all for extra discretion. + +- **On** by default. + ## Recommendations - **Hide / show notifications are off by default**: a notification on every hide would rather defeat the purpose, so they are not shown. Turn them on if you want explicit feedback. - **Start / exit / startup notifications are on by default**: these are infrequent events, and keeping them helps you confirm the core's state. ::: info Not the same as "Also hide the tray icon" -[Also hide the tray icon](/en/guide/options) controls whether **the icon itself is visible**; this page controls whether **notifications appear**. The two are independent. +[Also hide the tray icon](/en/guide/options) controls whether **the icon itself is visible**; this page controls **notifications and icon badges**. They are independent. ::: diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index ab820db..317824f 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -16,7 +16,7 @@ title: 快速上手 | --- | --- | --- | | **窗口绑定** | 选择要隐藏的窗口 / 进程 | [绑定窗口与进程](/guide/binding) | | **热键与鼠标** | 键盘热键、鼠标连击、四角手势、空闲自动隐藏 | [热键与鼠标手势](/guide/hotkeys) | -| **通知设置** | 逐事件控制托盘气泡通知 | [通知设置](/guide/notifications) | +| **提示设置** | 控制气泡通知与图标状态角标 | [提示设置](/guide/notifications) | | **其他选项** | 静音、暂停键、冻结、权限、日志、恢复工具等 | [其他选项](/guide/options) | | **关于与反馈** | 版本信息、检查更新、公告、问题反馈 | [检查更新与反馈](/guide/update) | @@ -72,6 +72,7 @@ title: 快速上手 - **设置**:打开配置界面。 - **隐藏窗口 / 显示窗口**:快速隐藏或显示已绑定的窗口。 - **窗口恢复工具**:打开配置页面并切换至窗口恢复工具,查看[窗口恢复与崩溃防护](/guide/recovery)。 +- **自动隐藏**:暂停 / 启用[空闲自动隐藏](/guide/hotkeys#空闲自动隐藏),勾选标记表示当前已启用。 - **开机自启**:切换是否随系统登录自动启动。 - **退出**:结束核心进程。 diff --git a/docs/guide/hotkeys.md b/docs/guide/hotkeys.md index 2ce8864..3c83958 100644 --- a/docs/guide/hotkeys.md +++ b/docs/guide/hotkeys.md @@ -92,6 +92,7 @@ Boss Key 默认使用系统的 `RegisterHotKey` 注册全局热键。若某个 开启 **启用自动隐藏** 后,当键盘和鼠标在设定时长内**无任何操作**时,Boss Key 会自动隐藏已绑定的窗口——适合临时离开工位的场景。 - **空闲时长**:无操作达到该时长(分钟)后触发,默认 **5 分钟**。 +- **托盘菜单快速切换**:右键托盘图标,点击 **自动隐藏** 菜单项即可暂停 / 启用自动隐藏,无需打开设置界面;勾选标记表示当前已启用,切换结果与设置界面保持同步。托盘图标还可叠加角标显示该状态,见[图标状态提示](/guide/notifications#图标状态提示)。 ## 小结 diff --git a/docs/guide/notifications.md b/docs/guide/notifications.md index 20413f4..1d62659 100644 --- a/docs/guide/notifications.md +++ b/docs/guide/notifications.md @@ -1,10 +1,10 @@ --- -title: 通知设置 +title: 提示设置 --- -# 通知设置 +# 提示设置 -Boss Key 会在一些事件发生时弹出**托盘气泡通知**。在配置界面的 **通知设置** 标签页,你可以控制是否弹出通知,做到既知情又不打扰。 +Boss Key 通过**托盘气泡通知**与**托盘图标角标**两类提示反映核心状态。在配置界面的 **提示设置** 标签页,你可以控制各类通知是否弹出、图标角标显示哪些状态,做到既知情又不打扰。 ## 可控制的通知事件 @@ -16,11 +16,35 @@ Boss Key 会在一些事件发生时弹出**托盘气泡通知**。在配置界 | **隐藏窗口** | 每次隐藏窗口时提示 | 关闭 | | **显示窗口** | 每次显示窗口时提示 | 关闭 | +## 图标状态提示 + +托盘图标可在**右下角叠加一个彩色圆点角标**,直观反映核心当前状态。四种颜色各自绑定一个状态源,默认绑定如下: + +| 颜色 | 默认绑定状态 | +| --- | --- | +| 红色 | 存在隐藏中的窗口 | +| 绿色 | 启用了自动隐藏 | +| 黄色 | 启用了同时隐藏当前窗口 | +| 蓝色 | 启用了进程冻结 | + +- 每种颜色都可以改绑为其他状态(除上表四种外,还可绑定**以管理员身份运行**、**热键监控已暂停**),或选择「不显示」置空该颜色。 +- 多个绑定状态同时满足时,按 **红 > 绿 > 黄 > 蓝** 的优先级只显示一个圆点。 + +::: warning 隐蔽性提示 +角标(尤其是「存在隐藏中的窗口」)也会向旁人透露核心状态。若你更在意隐蔽性,可将相应颜色置空。 +::: + +## 显示图标悬浮名称 + +鼠标悬停在托盘图标上时默认显示「Boss Key」。关闭该选项后,悬停不显示任何文字,更隐蔽。 + +- 默认**开启**。 + ## 使用建议 - **隐藏 / 显示通知默认关闭**:因为摸鱼场景下,每次隐藏都弹通知反而容易"暴露",因此默认不提示。若你希望有明确反馈可自行开启。 - **启动 / 退出 / 自启通知默认开启**:这些是低频事件,保留提示有助于你确认核心状态。 ::: info 与"隐藏托盘图标"的区别 -[隐藏后同时隐藏托盘图标](/guide/options#隐藏后同时隐藏托盘图标) 控制的是**图标本身是否可见**;本页控制的是**气泡通知是否弹出**,两者相互独立。 +[隐藏后同时隐藏托盘图标](/guide/options#隐藏后同时隐藏托盘图标) 控制的是**图标本身是否可见**;本页控制的是**气泡通知与图标角标**,三者相互独立。 ::: diff --git a/docs/zh-tw/dev/config-reference.md b/docs/zh-tw/dev/config-reference.md index 79c2448..a4ae92f 100644 --- a/docs/zh-tw/dev/config-reference.md +++ b/docs/zh-tw/dev/config-reference.md @@ -49,6 +49,8 @@ Boss Key 的設定儲存在與執行檔**同資料夾**的 `config.json` 中。* | `hide_current` | bool | `true` | [同時隱藏目前使用中的視窗](/zh-tw/guide/options) | | `click_to_hide` | bool | `true` | [按一下通知區域圖示切換隱藏](/zh-tw/guide/options) | | `hide_icon_after_hide` | bool | `false` | [隱藏後一併隱藏通知區域圖示](/zh-tw/guide/options) | +| `tray_badges` | object | 見下 | [圖示狀態提示](/zh-tw/guide/notifications#圖示狀態提示) | +| `tray_show_tooltip` | bool | `true` | [顯示圖示懸浮名稱](/zh-tw/guide/notifications#顯示圖示懸浮名稱) | | `freeze_after_hide` | bool | `false` | [程序凍結總開關](/zh-tw/guide/freeze) | | `enhanced_freeze` | bool | `false` | [增強凍結](/zh-tw/guide/freeze) | | `freeze_whole_tree` | bool | `false` | [凍結完整程序](/zh-tw/guide/freeze) | @@ -85,6 +87,19 @@ Boss Key 的設定儲存在與執行檔**同資料夾**的 `config.json` 中。* 全新安裝預設開啟**中鍵按一下**(`middle.enabled = true`,`clicks = 1`),其餘四顆關閉。設定檔缺 `mouse` 一節的舊設定讀進來則**全關**。 ::: +### `setting.tray_badges` + +[圖示狀態提示](/zh-tw/guide/notifications#圖示狀態提示):四種顏色的圓點角標各自繫結一個狀態來源,多個狀態同時活躍時依**紅 > 綠 > 黃 > 藍**的優先順序僅顯示一個圓點。 + +| 欄位 | 預設 | 預設含義 | +| --- | --- | --- | +| `red` | `"hidden"` | 存在隱藏中的視窗 | +| `green` | `"auto_hide"` | 已啟用自動隱藏 | +| `yellow` | `"hide_current"` | 已啟用同時隱藏目前視窗 | +| `blue` | `"freeze"` | 已啟用程序凍結 | + +每項取值:`hidden`(存在隱藏中的視窗)|`auto_hide`(已啟用自動隱藏)|`hide_current`(已啟用同時隱藏目前視窗)|`freeze`(已啟用程序凍結)|`elevated`(以系統管理員身分執行)|`monitor_paused`(快速鍵監控已暫停)|`""`(留空 = 不顯示該顏色);未知取值讀取時正規化為留空。 + ## `notifications` | 欄位 | 預設 | 說明 | diff --git a/docs/zh-tw/dev/frontend.md b/docs/zh-tw/dev/frontend.md index 1c9ee78..c807bd9 100644 --- a/docs/zh-tw/dev/frontend.md +++ b/docs/zh-tw/dev/frontend.md @@ -43,7 +43,7 @@ apps/config/ui/ | --- | --- | --- | | 視窗綁定 | `BindingPanel` | 視窗清單 + 視窗/程序規則 | | 快速鍵與滑鼠 | `HotkeysPanel` | 鍵盤快速鍵、滑鼠連按、四角、閒置自動隱藏 | -| 通知設定 | `NotificationsPanel` | 逐事件通知開關 | +| 提示設定 | `NotificationsPanel` | 逐事件通知開關與圖示狀態角標 | | 其他選項 | `OptionsPanel` | 靜音/暫停/凍結/權限/記錄檔/工具 | | 關於與意見回饋 | `AboutPanel` | 版本、更新、公告、意見回饋 | diff --git a/docs/zh-tw/guide/getting-started.md b/docs/zh-tw/guide/getting-started.md index dd541fc..99ae408 100644 --- a/docs/zh-tw/guide/getting-started.md +++ b/docs/zh-tw/guide/getting-started.md @@ -16,7 +16,7 @@ title: 快速上手 | --- | --- | --- | | **視窗綁定** | 選擇要隱藏的視窗/程序 | [綁定視窗與程序](/zh-tw/guide/binding) | | **快速鍵與滑鼠** | 鍵盤快速鍵、滑鼠連按、四角手勢、閒置自動隱藏 | [快速鍵與滑鼠手勢](/zh-tw/guide/hotkeys) | -| **通知設定** | 逐事件控制通知區域通知 | [通知設定](/zh-tw/guide/notifications) | +| **提示設定** | 控制氣泡通知與圖示狀態角標 | [提示設定](/zh-tw/guide/notifications) | | **其他選項** | 靜音、暫停鍵、凍結、權限、記錄檔、復原工具等 | [其他選項](/zh-tw/guide/options) | | **關於與意見回饋** | 版本資訊、檢查更新、公告、問題回報 | [檢查更新與意見回饋](/zh-tw/guide/update) | @@ -72,6 +72,7 @@ title: 快速上手 - **設定**:開啟設定介面。 - **隱藏視窗/顯示視窗**:快速隱藏或顯示已綁定的視窗。 - **視窗復原工具**:開啟設定頁面並切換至視窗復原工具,參閱[視窗復原與當機防護](/zh-tw/guide/recovery)。 +- **自動隱藏**:暫停/啟用[閒置自動隱藏](/zh-tw/guide/hotkeys#閒置自動隱藏),勾選標記表示目前已啟用。 - **開機自動啟動**:切換是否隨系統登入自動啟動。 - **結束**:結束核心程序。 diff --git a/docs/zh-tw/guide/hotkeys.md b/docs/zh-tw/guide/hotkeys.md index d279e40..437d2b0 100644 --- a/docs/zh-tw/guide/hotkeys.md +++ b/docs/zh-tw/guide/hotkeys.md @@ -92,6 +92,7 @@ Boss Key 預設使用系統的 `RegisterHotKey` 註冊全域快速鍵。若某 開啟 **啟用自動隱藏** 後,當鍵盤和滑鼠在設定時間內**無任何操作**時,Boss Key 會自動隱藏已綁定的視窗。 - **閒置時間**:無操作達到該時間(分鐘)後觸發,預設 **5 分鐘**。 +- **通知區域選單快速切換**:以右鍵按一下通知區域圖示,點選 **自動隱藏** 選單項目即可暫停/啟用自動隱藏,不需開啟設定介面;勾選標記表示目前已啟用,切換結果與設定介面保持同步。通知區域圖示還可疊加角標顯示該狀態,見[圖示狀態提示](/zh-tw/guide/notifications#圖示狀態提示)。 ## 小結 diff --git a/docs/zh-tw/guide/notifications.md b/docs/zh-tw/guide/notifications.md index 233c3d9..837f00c 100644 --- a/docs/zh-tw/guide/notifications.md +++ b/docs/zh-tw/guide/notifications.md @@ -1,10 +1,10 @@ --- -title: 通知設定 +title: 提示設定 --- -# 通知設定 +# 提示設定 -Boss Key 會在一些事件發生時顯示**通知區域通知**。在設定介面的 **通知設定** 標籤頁,您可以控制是否顯示通知,做到既知情又不打擾。 +Boss Key 透過**通知區域氣泡通知**與**圖示角標**兩類提示反映核心狀態。在設定介面的 **提示設定** 標籤頁,您可以控制各類通知是否顯示、圖示角標顯示哪些狀態,做到既知情又不打擾。 ## 可控制的通知事件 @@ -16,11 +16,35 @@ Boss Key 會在一些事件發生時顯示**通知區域通知**。在設定介 | **隱藏視窗** | 每次隱藏視窗時提示 | 關閉 | | **顯示視窗** | 每次顯示視窗時提示 | 關閉 | +## 圖示狀態提示 + +通知區域圖示可在**右下角疊加一個彩色圓點角標**,直觀反映核心目前狀態。四種顏色各自繫結一個狀態來源,預設繫結如下: + +| 顏色 | 預設繫結狀態 | +| --- | --- | +| 紅色 | 存在隱藏中的視窗 | +| 綠色 | 已啟用自動隱藏 | +| 黃色 | 已啟用同時隱藏目前視窗 | +| 藍色 | 已啟用程序凍結 | + +- 每種顏色都可以改繫結為其他狀態(除上表四種外,還可繫結**以系統管理員身分執行**、**快速鍵監控已暫停**),或選擇「不顯示」停用該顏色。 +- 多個繫結狀態同時滿足時,依 **紅 > 綠 > 黃 > 藍** 的優先順序僅顯示一個圓點。 + +::: warning 隱密性提示 +角標(尤其是「存在隱藏中的視窗」)也會向旁人透露核心狀態。若您更在意隱密性,可停用相應顏色。 +::: + +## 顯示圖示懸浮名稱 + +滑鼠停留在通知區域圖示上時預設顯示「Boss Key」。關閉該選項後,停留不顯示任何文字,更為隱密。 + +- 預設**開啟**。 + ## 使用建議 - **隱藏/顯示通知預設關閉**:因為摸魚情境下,每次隱藏都顯示通知反而容易「曝光」,因此預設不提示。若您希望有明確回饋可自行開啟。 - **啟動/結束/自動啟動通知預設開啟**:這些是低頻事件,保留提示有助於您確認核心狀態。 ::: info 與「隱藏通知區域圖示」的區別 -[隱藏後一併隱藏通知區域圖示](/zh-tw/guide/options) 控制的是**圖示本身是否可見**;本頁控制的是**通知是否顯示**,兩者相互獨立。 +[隱藏後一併隱藏通知區域圖示](/zh-tw/guide/options) 控制的是**圖示本身是否可見**;本頁控制的是**氣泡通知與圖示角標**,三者相互獨立。 ::: diff --git a/qodana.sarif.json b/qodana.sarif.json new file mode 100644 index 0000000..6f65109 --- /dev/null +++ b/qodana.sarif.json @@ -0,0 +1,27547 @@ +{ + "$schema": "https://raw.githubusercontent.com/schemastore/schemastore/master/src/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "RR", + "fullName": "Qodana", + "version": "261.26222.73", + "rules": [], + "taxa": [ + { + "id": "JavaScript 和 TypeScript", + "name": "JavaScript 和 TypeScript" + }, + { + "id": "JavaScript 和 TypeScript/控制流问题", + "name": "控制流问题", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HTTP 客户端", + "name": "HTTP 客户端" + }, + { + "id": "Rust", + "name": "Rust" + }, + { + "id": "Rust/Lint", + "name": "Lint", + "relationships": [ + { + "target": { + "id": "Rust", + "index": 3, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "Dockerfile", + "name": "Dockerfile" + }, + { + "id": "Rust/Lints", + "name": "Lints", + "relationships": [ + { + "target": { + "id": "Rust", + "index": 3, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "Rust/Lints/命名约定", + "name": "命名约定", + "relationships": [ + { + "target": { + "id": "Rust/Lints", + "index": 6, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "Angular", + "name": "Angular" + }, + { + "id": "PostCSS", + "name": "PostCSS" + }, + { + "id": "Svelte", + "name": "Svelte" + }, + { + "id": "XPath", + "name": "XPath" + }, + { + "id": "Sass_SCSS", + "name": "Sass/SCSS" + }, + { + "id": "Shell 脚本", + "name": "Shell 脚本" + }, + { + "id": "JavaScript 和 TypeScript/单元测试", + "name": "单元测试", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "校对", + "name": "校对" + }, + { + "id": "JSON 和 JSON5", + "name": "JSON 和 JSON5" + }, + { + "id": "MongoJS", + "name": "MongoJS" + }, + { + "id": "JavaScript 和 TypeScript/常规", + "name": "常规", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JavaScript 和 TypeScript/代码样式问题", + "name": "代码样式问题", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MySQL", + "name": "MySQL" + }, + { + "id": "JavaScript 和 TypeScript/可能不合需要的代码结构", + "name": "可能不合需要的代码结构", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JavaScript 和 TypeScript/Flow 类型检查器", + "name": "Flow 类型检查器", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "Markdown", + "name": "Markdown" + }, + { + "id": "JetBrains Academy", + "name": "JetBrains Academy" + }, + { + "id": "JetBrains Academy/Yaml 配置", + "name": "Yaml 配置", + "relationships": [ + { + "target": { + "id": "JetBrains Academy", + "index": 24, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JavaScript 和 TypeScript/可能的 bug", + "name": "可能的 bug", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HTML", + "name": "HTML" + }, + { + "id": "JavaScript 和 TypeScript/未使用的符号", + "name": "未使用的符号", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JavaScript 和 TypeScript/数据流", + "name": "数据流", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JavaScript 和 TypeScript/按位运算问题", + "name": "按位运算问题", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JavaScript 和 TypeScript/ES2015 迁移协助", + "name": "ES2015 迁移协助", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "常规", + "name": "常规" + }, + { + "id": "HTML/可访问性", + "name": "可访问性", + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JavaScript 和 TypeScript/React", + "name": "React", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JavaScript 和 TypeScript/TypeScript", + "name": "TypeScript", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JavaScript 和 TypeScript/有效性问题", + "name": "有效性问题", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "Rust/Cargo.toml", + "name": "Cargo.toml", + "relationships": [ + { + "target": { + "id": "Rust", + "index": 3, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "Docker-compose", + "name": "Docker-compose" + }, + { + "id": "JavaScript 和 TypeScript/可能引起混淆的代码结构", + "name": "可能引起混淆的代码结构", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SQL", + "name": "SQL" + }, + { + "id": "CSS", + "name": "CSS" + }, + { + "id": "CSS/无效元素", + "name": "无效元素", + "relationships": [ + { + "target": { + "id": "CSS", + "index": 41, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JavaScript 和 TypeScript/Try 语句问题", + "name": "Try 语句问题", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "结构搜索", + "name": "结构搜索" + }, + { + "id": "JavaScript 和 TypeScript/函数指标", + "name": "函数指标", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "YAML", + "name": "YAML" + }, + { + "id": "XML", + "name": "XML" + }, + { + "id": "JavaScript 和 TypeScript/赋值问题", + "name": "赋值问题", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CSS/代码样式问题", + "name": "代码样式问题", + "relationships": [ + { + "target": { + "id": "CSS", + "index": 41, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "Less", + "name": "Less" + }, + { + "id": "正则表达式", + "name": "正则表达式" + }, + { + "id": "Vue", + "name": "Vue" + }, + { + "id": "JavaScript 和 TypeScript/Node.js", + "name": "Node.js", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XSLT", + "name": "XSLT" + }, + { + "id": "JavaScript 和 TypeScript/Import 和依赖项", + "name": "Import 和依赖项", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RELAX NG", + "name": "RELAX NG" + }, + { + "id": "CSS/可能的 bug", + "name": "可能的 bug", + "relationships": [ + { + "target": { + "id": "CSS", + "index": 41, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JavaScript 和 TypeScript/命名约定", + "name": "命名约定", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JavaScript 和 TypeScript/switch 语句问题", + "name": "switch 语句问题", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JavaScript 和 TypeScript/DOM 问题", + "name": "DOM 问题", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JavaScript 和 TypeScript/Async 代码和 promise", + "name": "Async 代码和 promise", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JavaScript 和 TypeScript/代码质量工具", + "name": "代码质量工具", + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript", + "index": 0, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "代码覆盖率", + "name": "代码覆盖率" + }, + { + "id": "GitHub 操作", + "name": "GitHub 操作" + }, + { + "id": "GitLab CI_CD", + "name": "GitLab CI/CD" + }, + { + "id": "Qodana", + "name": "Qodana" + }, + { + "id": "CSS/代码质量工具", + "name": "代码质量工具", + "relationships": [ + { + "target": { + "id": "CSS", + "index": 41, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "PostgreSQL", + "name": "PostgreSQL" + }, + { + "id": "SQL Server", + "name": "SQL Server" + }, + { + "id": "Dev Container", + "name": "Dev Container" + }, + { + "id": "JSONPath", + "name": "JSONPath" + }, + { + "id": "安全性", + "name": "安全性" + }, + { + "id": "Oracle", + "name": "Oracle" + }, + { + "id": "国际化", + "name": "国际化" + }, + { + "id": "TOML", + "name": "TOML" + }, + { + "id": "版本控制", + "name": "版本控制" + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + "extensions": [ + { + "name": "JavaScript", + "version": "261.26222.73", + "rules": [ + { + "id": "ConstantConditionalExpressionJS", + "shortDescription": { + "text": "常量条件表达式" + }, + "fullDescription": { + "text": "报告格式为 'true? 的条件表达式 result1: result2' 或 'false? result1: result2。 建议简化该表达式。 Inspection ID: ConstantConditionalExpressionJS'", + "markdown": "报告格式为 `true? 的条件表达式 result1: result2` 或 `false? result1: result2``。\n建议简化该表达式。\n`\n\nInspection ID: ConstantConditionalExpressionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ConstantConditionalExpressionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/控制流问题", + "index": 1, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSTestFailedLine", + "shortDescription": { + "text": "在测试代码中高亮显示失败的行" + }, + "fullDescription": { + "text": "报告失败的方法调用或测试中的断言。 Inspection ID: JSTestFailedLine", + "markdown": "报告失败的方法调用或测试中的断言。\n\nInspection ID: JSTestFailedLine" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSTestFailedLine", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/单元测试", + "index": 14, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSValidateJSDoc", + "shortDescription": { + "text": "JSDoc 中的语法错误和未解析的引用" + }, + "fullDescription": { + "text": "报告文档注释中的语法差异。 Inspection ID: JSValidateJSDoc", + "markdown": "报告文档注释中的语法差异。\n\nInspection ID: JSValidateJSDoc" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSValidateJSDoc", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "IfStatementWithTooManyBranchesJS", + "shortDescription": { + "text": "'if' 语句的分支过多" + }, + "fullDescription": { + "text": "报告分支过多的 'if' 语句。 此类语句可能令人困惑,并且通常表明设计抽象级别不足。 使用下面的字段可指定预期的最大分支数。 Inspection ID: IfStatementWithTooManyBranchesJS", + "markdown": "报告分支过多的 `if` 语句。 此类语句可能令人困惑,并且通常表明设计抽象级别不足。\n\n\n使用下面的字段可指定预期的最大分支数。\n\nInspection ID: IfStatementWithTooManyBranchesJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "IfStatementWithTooManyBranchesJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/控制流问题", + "index": 1, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "NonBlockStatementBodyJS", + "shortDescription": { + "text": "没有大括号的语句体" + }, + "fullDescription": { + "text": "报告其语句体不是块语句的 'if'、'while'、'for' 或 'with' 语句。 在语句体中使用代码块通常对下游维护更安全。 Inspection ID: NonBlockStatementBodyJS", + "markdown": "报告其语句体不是块语句的 `if`、`while`、`for` 或 `with` 语句。 在语句体中使用代码块通常对下游维护更安全。\n\nInspection ID: NonBlockStatementBodyJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "NonBlockStatementBodyJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/代码样式问题", + "index": 19, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "BreakStatementJS", + "shortDescription": { + "text": "'break' 语句" + }, + "fullDescription": { + "text": "报告 'break' 语句。 忽略结束 case 块的 'break' 语句。 Inspection ID: BreakStatementJS", + "markdown": "报告 `break` 语句。 忽略结束 case 块的 `break` 语句。\n\nInspection ID: BreakStatementJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "BreakStatementJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能不合需要的代码结构", + "index": 21, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "FlowJSConfig", + "shortDescription": { + "text": "缺少 .flowconfig" + }, + "fullDescription": { + "text": "报告在项目中没有关联的 '.flowconfig' 文件且带有 '@flow' 标志的 JavaScript 文件。 Inspection ID: FlowJSConfig", + "markdown": "报告在项目中没有关联的 `.flowconfig` 文件且带有 `@flow` 标志的 JavaScript 文件。\n\nInspection ID: FlowJSConfig" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "FlowJSConfig", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Flow 类型检查器", + "index": 22, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSPotentiallyInvalidUsageOfClassThis", + "shortDescription": { + "text": "从闭包对类中 'this' 的引用可能无效" + }, + "fullDescription": { + "text": "报告试图通过不是 lambda 的嵌套函数中的 'this.' 限定符,来引用 ECMAScript 类成员的情况。 不是 lambda 的嵌套函数中的 'this' 是函数自身的 'this',与外部类无关。 Inspection ID: JSPotentiallyInvalidUsageOfClassThis", + "markdown": "报告试图通过不是 lambda 的嵌套函数中的 `this.` 限定符,来引用 ECMAScript 类成员的情况。 \n不是 lambda 的嵌套函数中的 `this` 是函数自身的 ` this`,与外部类无关。\n\nInspection ID: JSPotentiallyInvalidUsageOfClassThis" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSPotentiallyInvalidUsageOfClassThis", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能的 bug", + "index": 26, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "DebuggerStatementJS", + "shortDescription": { + "text": "'debugger' 语句" + }, + "fullDescription": { + "text": "报告用于与 Javascript 调试器交互的 'debugger' 语句。 此类语句不应出现在生产代码中。 Inspection ID: DebuggerStatementJS", + "markdown": "报告用于与 Javascript 调试器交互的 `debugger` 语句。 此类语句不应出现在生产代码中。\n\nInspection ID: DebuggerStatementJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "DebuggerStatementJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能不合需要的代码结构", + "index": 21, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSUnusedAssignment", + "shortDescription": { + "text": "未使用的赋值" + }, + "fullDescription": { + "text": "报告赋值后从未使用其值的变量。 建议移除未使用的变量以缩短代码并避免冗余分配。 将报告以下情况: 变量赋值后从未读取。 在下次读取变量前,变量的值总是被另一个赋值覆盖。 变量的初始值设定项冗余(出于上述原因之一)。 Inspection ID: JSUnusedAssignment", + "markdown": "报告赋值后从未使用其值的变量。 \n建议移除未使用的变量以缩短代码并避免冗余分配。\n\n将报告以下情况:\n\n* 变量赋值后从未读取。\n* 在下次读取变量前,变量的值总是被另一个赋值覆盖。\n* 变量的初始值设定项冗余(出于上述原因之一)。\n\nInspection ID: JSUnusedAssignment" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSUnusedAssignment", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Performance" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/未使用的符号", + "index": 28, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ReuseOfLocalVariableJS", + "shortDescription": { + "text": "局部变量的重用" + }, + "fullDescription": { + "text": "报告重用局部变量并使用无关 初始变量用法的新值覆盖其值的情况。 以这种方式重用局部变量可能会造成混淆, 因为局部变量的预期语义可能随每种用法而变化。 如果代码更改导致预期被覆盖的值任然存在,那么也可能导致错误。 优良的作法是保持变量生命周期尽可能短,而不要为了简洁重用局部变量。 Inspection ID: ReuseOfLocalVariableJS", + "markdown": "报告重用局部变量并使用无关 初始变量用法的新值覆盖其值的情况。 以这种方式重用局部变量可能会造成混淆, 因为局部变量的预期语义可能随每种用法而变化。 如果代码更改导致预期被覆盖的值任然存在,那么也可能导致错误。 优良的作法是保持变量生命周期尽可能短,而不要为了简洁重用局部变量。\n\nInspection ID: ReuseOfLocalVariableJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ReuseOfLocalVariableJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/数据流", + "index": 29, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ShiftOutOfRangeJS", + "shortDescription": { + "text": "通过可能错误的常量进行移位运算" + }, + "fullDescription": { + "text": "报告此类情况下的移位运算,即第二个操作数是合理范围外的常量,例如 '0..31' 范围外的整数移位运算,移位的值为负值或过大的值。 Inspection ID: ShiftOutOfRangeJS", + "markdown": "报告此类情况下的移位运算,即第二个操作数是合理范围外的常量,例如 `0..31` 范围外的整数移位运算,移位的值为负值或过大的值。\n\nInspection ID: ShiftOutOfRangeJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ShiftOutOfRangeJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/按位运算问题", + "index": 30, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSClosureCompilerSyntax", + "shortDescription": { + "text": "JSDoc 标记不正确的用法" + }, + "fullDescription": { + "text": "报告 Google Closure Compiler 注解隐含的警告,包括 '@abstract'、'@interface' 和 '@implement' 标记的正确使用情况。 Inspection ID: JSClosureCompilerSyntax", + "markdown": "报告 *Google Closure Compiler* 注解隐含的警告,包括 `@abstract`、`@interface` 和 `@implement` 标记的正确使用情况。\n\nInspection ID: JSClosureCompilerSyntax" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSClosureCompilerSyntax", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "UnnecessaryContinueJS", + "shortDescription": { + "text": "不必要的 'continue' 语句" + }, + "fullDescription": { + "text": "报告循环末尾不必要的 'continue' 语句。 建议移除这些语句。 Inspection ID: UnnecessaryContinueJS", + "markdown": "报告循环末尾不必要的 ` continue` 语句。 建议移除这些语句。\n\nInspection ID: UnnecessaryContinueJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "UnnecessaryContinueJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/控制流问题", + "index": 1, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ES6ConvertLetToConst", + "shortDescription": { + "text": "已使用 'let' 而不是 'const'" + }, + "fullDescription": { + "text": "报告可以设为 'const' 的 'let' 声明。 Inspection ID: ES6ConvertLetToConst", + "markdown": "报告可以设为 `const` 的 `let` 声明。 \n\nInspection ID: ES6ConvertLetToConst" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "ES6ConvertLetToConst", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/ES2015 迁移协助", + "index": 31, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSXDomNesting", + "shortDescription": { + "text": "无效的 DOM 元素嵌套" + }, + "fullDescription": { + "text": "检测 JSX 文件中未根据 DOM 规范正确嵌套的 HTML 元素。 React 会对不正确嵌套的元素报告运行时警告。 Inspection ID: JSXDomNesting", + "markdown": "检测 JSX 文件中未根据 DOM 规范正确嵌套的 HTML 元素。 React 会对不正确嵌套的元素报告运行时警告。\n\nInspection ID: JSXDomNesting" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSXDomNesting", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/React", + "index": 34, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TypeScriptValidateTypes", + "shortDescription": { + "text": "类型不匹配" + }, + "fullDescription": { + "text": "报告类型不正确的形参、返回值或赋值表达式。 Inspection ID: TypeScriptValidateTypes", + "markdown": "报告类型不正确的形参、返回值或赋值表达式。\n\nInspection ID: TypeScriptValidateTypes" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "TypeScriptValidateTypes", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/TypeScript", + "index": 35, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "BadExpressionStatementJS", + "shortDescription": { + "text": "非赋值或调用的表达式语句" + }, + "fullDescription": { + "text": "报告既非赋值也非调用的表达式语句。 此类语句通常表明有错误。 Inspection ID: BadExpressionStatementJS", + "markdown": "报告既非赋值也非调用的表达式语句。 此类语句通常表明有错误。\n\nInspection ID: BadExpressionStatementJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "BadExpressionStatementJS", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/有效性问题", + "index": 36, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ConfusingFloatingPointLiteralJS", + "shortDescription": { + "text": "浮点字面量会引起混淆" + }, + "fullDescription": { + "text": "报告任何没有小数点的浮点数或小数点前的任何数字 或小数点后的任何数字。 此类字面量可能令人困惑,并且违反多种编码标准。 Inspection ID: ConfusingFloatingPointLiteralJS", + "markdown": "报告任何没有小数点的浮点数或小数点前的任何数字 或小数点后的任何数字。 此类字面量可能令人困惑,并且违反多种编码标准。\n\nInspection ID: ConfusingFloatingPointLiteralJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ConfusingFloatingPointLiteralJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能引起混淆的代码结构", + "index": 39, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "BreakStatementWithLabelJS", + "shortDescription": { + "text": "带标签的 'break' 语句" + }, + "fullDescription": { + "text": "报告 'break' 标签语句。 Inspection ID: BreakStatementWithLabelJS", + "markdown": "报告 `break` 标签语句。\n\nInspection ID: BreakStatementWithLabelJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "BreakStatementWithLabelJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能不合需要的代码结构", + "index": 21, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ContinueOrBreakFromFinallyBlockJS", + "shortDescription": { + "text": "'continue' 或 'break' 位于 'finally' 块内" + }, + "fullDescription": { + "text": "报告 'finally' 块中的 'break' 或 'continue' 语句。 此类语句非常令人困惑,可能隐藏异常,并导致调试复杂化。 Inspection ID: ContinueOrBreakFromFinallyBlockJS", + "markdown": "报告 `finally` 块中的 `break` 或 `continue` 语句。 此类语句非常令人困惑,可能隐藏异常,并导致调试复杂化。\n\nInspection ID: ContinueOrBreakFromFinallyBlockJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ContinueOrBreakFromFinallyBlockJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Try 语句问题", + "index": 43, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "StatementsPerFunctionJS", + "shortDescription": { + "text": "函数过长" + }, + "fullDescription": { + "text": "报告过长的函数。 函数长度通过函数中非空语句的数量来计算。 太长的函数容易出错,而且难以测试。 使用下面的字段可指定函数中的可接受最大语句数量。 Inspection ID: StatementsPerFunctionJS", + "markdown": "报告过长的函数。 函数长度通过函数中非空语句的数量来计算。 太长的函数容易出错,而且难以测试。\n\n\n使用下面的字段可指定函数中的可接受最大语句数量。\n\nInspection ID: StatementsPerFunctionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "FunctionTooLongJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/函数指标", + "index": 45, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "UnnecessaryLocalVariableJS", + "shortDescription": { + "text": "冗余局部变量" + }, + "fullDescription": { + "text": "报告不会让函数更易于理解的不必要局部变量: 立刻返回的局部变量 立刻赋值给另一个变量并且不再使用的局部变量 总是与另一个局部变量或形参的值相同的局部变量。 使用下面的复选框以使该检查忽略立刻返回或抛出的变量。 为清晰起见和易于调试,某些编码样式建议使用此类变量。 Inspection ID: UnnecessaryLocalVariableJS", + "markdown": "报告不会让函数更易于理解的不必要局部变量:\n\n* 立刻返回的局部变量\n* 立刻赋值给另一个变量并且不再使用的局部变量\n* 总是与另一个局部变量或形参的值相同的局部变量。\n\n\n使用下面的复选框以使该检查忽略立刻返回或抛出的变量。 为清晰起见和易于调试,某些编码样式建议使用此类变量。\n\nInspection ID: UnnecessaryLocalVariableJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "UnnecessaryLocalVariableJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/数据流", + "index": 29, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSMethodCanBeStatic", + "shortDescription": { + "text": "方法可以为 'static'" + }, + "fullDescription": { + "text": "报告可以安全地设为 'static' 的类方法。 如果某个方法未引用其类的任何非 static 方法和非 static 字段,并且未在子类中被重写, 则该方法可以为 'static' 方法。 使用下面的第一个复选框只检查 'private' 方法。 Inspection ID: JSMethodCanBeStatic", + "markdown": "报告可以安全地设为 `static` 的类方法。 如果某个方法未引用其类的任何非 static 方法和非 static 字段,并且未在子类中被重写, 则该方法可以为 `static` 方法。\n\n\n使用下面的第一个复选框只检查 `private` 方法。\n\nInspection ID: JSMethodCanBeStatic" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSMethodCanBeStatic", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSDeclarationsAtScopeStart", + "shortDescription": { + "text": "'var' 未在函数开头声明" + }, + "fullDescription": { + "text": "检查使用 var 声明的局部变量的声明是否位于函数范围的顶部。 默认情况下,在执行代码时,变量声明总是被隐形地移动(“提升”)到它所包含的范围的顶部。 因此,在范围顶部声明有助于在代码中表现出这种行为。 Inspection ID: JSDeclarationsAtScopeStart", + "markdown": "检查使用 **var** 声明的局部变量的声明是否位于函数范围的顶部。 \n\n默认情况下,在执行代码时,变量声明总是被隐形地移动(\"提升\")到它所包含的范围的顶部。 因此,在范围顶部声明有助于在代码中表现出这种行为。\n\nInspection ID: JSDeclarationsAtScopeStart" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSDeclarationsAtScopeStart", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/代码样式问题", + "index": 19, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ContinueStatementWithLabelJS", + "shortDescription": { + "text": "带标签的 'continue' 语句" + }, + "fullDescription": { + "text": "添加 'continue' 标签语句。 Inspection ID: ContinueStatementWithLabelJS", + "markdown": "添加 `continue` 标签语句。\n\nInspection ID: ContinueStatementWithLabelJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ContinueStatementWithLabelJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能不合需要的代码结构", + "index": 21, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSObjectNullOrUndefined", + "shortDescription": { + "text": "对象为 'null' 或 'undefined'" + }, + "fullDescription": { + "text": "报告对 'undefined' 或 'null' 对象调用方法、访问属性或调用函数所导致的错误。 Inspection ID: JSObjectNullOrUndefined", + "markdown": "报告对 `undefined` 或 `null` 对象调用方法、访问属性或调用函数所导致的错误。\n\nInspection ID: JSObjectNullOrUndefined" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSObjectNullOrUndefined", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/控制流问题", + "index": 1, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TypeScriptMissingConfigOption", + "shortDescription": { + "text": "缺少 tsconfig.json 选项 " + }, + "fullDescription": { + "text": "报告 'tsconfig.json' 中需要显式选项的用法。 例如,要在 '.tsx' 文件中使用 JSX,'tsconfig.json' 必须包含 '\"jsx\"' 属性。 Inspection ID: TypeScriptMissingConfigOption", + "markdown": "报告 `tsconfig.json` 中需要显式选项的用法。 例如,要在 `.tsx` 文件中使用 JSX,`tsconfig.json` 必须包含 `\"jsx\"` 属性。\n\nInspection ID: TypeScriptMissingConfigOption" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "TypeScriptMissingConfigOption", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/TypeScript", + "index": 35, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSXUnresolvedComponent", + "shortDescription": { + "text": "未解析的 JSX 组件" + }, + "fullDescription": { + "text": "报告对 JSX 组件的未解析引用。 如果引用的组件在此项目或其依赖项中定义,则建议添加缺少的 import 语句,或创建采用此名称的新组件。 新组件的模板可以在“编辑器 | 文件和代码模板”中修改。 Inspection ID: JSXUnresolvedComponent", + "markdown": "报告对 JSX 组件的未解析引用。 如果引用的组件在此项目或其依赖项中定义,则建议添加缺少的 import 语句,或创建采用此名称的新组件。\n\n新组件的模板可以在\"编辑器 \\| 文件和代码模板\"中修改。\n\nInspection ID: JSXUnresolvedComponent" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSXUnresolvedComponent", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ES6ShorthandObjectProperty", + "shortDescription": { + "text": "属性可被替换为速记形式" + }, + "fullDescription": { + "text": "报告可以转换为 ES6 速记样式的对象属性,并提供快速修复来执行此操作。 示例: 'var obj = {foo:foo}' 应用快速修复后,代码如下所示: 'var obj = {foo}' Inspection ID: ES6ShorthandObjectProperty", + "markdown": "报告可以转换为 ES6 速记样式的对象属性,并提供快速修复来执行此操作。\n\n示例:\n\n\n var obj = {foo:foo}\n\n应用快速修复后,代码如下所示:\n\n\n var obj = {foo}\n\nInspection ID: ES6ShorthandObjectProperty" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "ES6ShorthandObjectProperty", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "UnnecessaryLabelOnBreakStatementJS", + "shortDescription": { + "text": "'break' 语句上的标签不必要" + }, + "fullDescription": { + "text": "报告可以在不更改控制流的情况下移除其标签的 'break' 标签语句。 Inspection ID: UnnecessaryLabelOnBreakStatementJS", + "markdown": "报告可以在不更改控制流的情况下移除其标签的 `break` 标签语句。\n\nInspection ID: UnnecessaryLabelOnBreakStatementJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "UnnecessaryLabelOnBreakStatementJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/控制流问题", + "index": 1, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ContinueStatementJS", + "shortDescription": { + "text": "'continue' 语句" + }, + "fullDescription": { + "text": "报告 'continue' 语句。 Inspection ID: ContinueStatementJS", + "markdown": "报告 `continue` 语句。\n\nInspection ID: ContinueStatementJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ContinueStatementJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能不合需要的代码结构", + "index": 21, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AssignmentToForLoopParameterJS", + "shortDescription": { + "text": "赋值给 'for' 循环形参" + }, + "fullDescription": { + "text": "报告对声明为 'for' 循环形参的变量的赋值。 虽然偶尔是有意为之,但此结构可能格外令人困惑,并且通常是由错误所致。 Inspection ID: AssignmentToForLoopParameterJS", + "markdown": "报告对声明为 ` for` 循环形参的变量的赋值。 虽然偶尔是有意为之,但此结构可能格外令人困惑,并且通常是由错误所致。\n\nInspection ID: AssignmentToForLoopParameterJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "AssignmentToForLoopParameterJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/赋值问题", + "index": 48, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSPotentiallyInvalidConstructorUsage", + "shortDescription": { + "text": "可能无效的构造函数用法" + }, + "fullDescription": { + "text": "报告可能无效的构造函数的用法,例如:'new' 后面不是构造函数的函数,使用 构造函数的原型或调用构造函数但没有使用 'new' 。 假定构造函数的名称采用大写(可选)或有显式的 JSDoc '@constructor' 标记。 Inspection ID: JSPotentiallyInvalidConstructorUsage", + "markdown": "报告可能无效的构造函数的用法,例如:`new` 后面不是构造函数的函数,使用 构造函数的原型或调用构造函数但没有使用 `new` 。 假定构造函数的名称采用大写(可选)或有显式的 JSDoc `@constructor` 标记。\n\nInspection ID: JSPotentiallyInvalidConstructorUsage" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSPotentiallyInvalidConstructorUsage", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能的 bug", + "index": 26, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "PointlessArithmeticExpressionJS", + "shortDescription": { + "text": "无意义的算术表达式" + }, + "fullDescription": { + "text": "报告包括加零或减零、乘零或一、除一和零移位的算术表达式。 此类表达式可能是由于没有完全完成自动化重构而造成。 Inspection ID: PointlessArithmeticExpressionJS", + "markdown": "报告包括加零或减零、乘零或一、除一和零移位的算术表达式。 此类表达式可能是由于没有完全完成自动化重构而造成。\n\nInspection ID: PointlessArithmeticExpressionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "PointlessArithmeticExpressionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能引起混淆的代码结构", + "index": 39, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "NodeCoreCodingAssistance", + "shortDescription": { + "text": "未解析的 Node.js API" + }, + "fullDescription": { + "text": "建议为 Node.js 配置编码辅助,例如 'require' 和/或核心模块('path'、'http'、'fs' 等)。 有关完整列表,请参阅 https://nodejs.org/api/。 Inspection ID: NodeCoreCodingAssistance", + "markdown": "建议为 Node.js 配置编码辅助,例如 `require` 和/或核心模块('path'、'http'、'fs' 等)。\n\n\n有关完整列表,请参阅 。\n\nInspection ID: NodeCoreCodingAssistance" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "NodeCoreCodingAssistance", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Node.js", + "index": 53, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSUndeclaredVariable", + "shortDescription": { + "text": "隐式声明的全局 JavaScript 变量" + }, + "fullDescription": { + "text": "报告全局变量的隐式声明。 示例: 'var aaa = 1; // 优良\n bbb = 2; // 不良,如果未在某处用 'var' 声明 bbb' Inspection ID: JSUndeclaredVariable", + "markdown": "报告全局变量的隐式声明。\n\n示例:\n\n\n var aaa = 1; // 优良\n bbb = 2; // 不良,如果未在某处用 'var' 声明 bbb\n\nInspection ID: JSUndeclaredVariable" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSUndeclaredVariable", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "DivideByZeroJS", + "shortDescription": { + "text": "除以零" + }, + "fullDescription": { + "text": "报告除以 0 或对 0 取余。 Inspection ID: DivideByZeroJS", + "markdown": "报告除以 0 或对 0 取余。\n\nInspection ID: DivideByZeroJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "DivideByZeroJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能的 bug", + "index": 26, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSPrimitiveTypeWrapperUsage", + "shortDescription": { + "text": "使用了基元类型对象包装器" + }, + "fullDescription": { + "text": "报告基元类型包装器使用不当或基元类型属性被修改的情况,因为在后一种情况下,所赋之值将丢失。 Inspection ID: JSPrimitiveTypeWrapperUsage", + "markdown": "报告基元类型包装器使用不当或基元类型属性被修改的情况,因为在后一种情况下,所赋之值将丢失。\n\nInspection ID: JSPrimitiveTypeWrapperUsage" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSPrimitiveTypeWrapperUsage", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TypeScriptSmartCast", + "shortDescription": { + "text": "范围缩小的类型" + }, + "fullDescription": { + "text": "报告通过类型防护缩小变量类型范围时的变量用法。 请注意,严重性级别不影响此检查。 Inspection ID: TypeScriptSmartCast", + "markdown": "报告通过类型防护缩小变量类型范围时的变量用法。 请注意,严重性级别不影响此检查。\n\nInspection ID: TypeScriptSmartCast" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "TypeScriptSmartCast", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/TypeScript", + "index": 35, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ES6ConvertIndexedForToForOf", + "shortDescription": { + "text": "已使用索引的 'for' 而不是 'for..of'" + }, + "fullDescription": { + "text": "报告对数组使用的 'for' 索引循环。 建议将其替换为 'for..of' 循环。 'for..of' 循环 (在 ECMAScript 6 中引入) 对 'iterable' 对象进行迭代。 Inspection ID: ES6ConvertIndexedForToForOf", + "markdown": "报告对数组使用的 [for](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for) 索引循环。 建议将其替换为 [for..of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of) 循环。 \n`for..of` 循环 (在 ECMAScript 6 中引入) 对 `iterable` 对象进行迭代。\n\nInspection ID: ES6ConvertIndexedForToForOf" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "ES6ConvertIndexedForToForOf", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/ES2015 迁移协助", + "index": 31, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSLastCommaInArrayLiteral", + "shortDescription": { + "text": "数组字面量中的最后一个逗号多余" + }, + "fullDescription": { + "text": "报告数组字面量中尾随逗号的用法。 只有将 JavaScript 语言版本设置为 ECMAScript 5.1 时才报告该警告。 尽管此规范允许在数组中使用尾随逗号,但在使用尾随逗号时,某些浏览器可能会抛出错误。 您可以在代码样式 | JavaScript 或 TypeScript | 标点符号中配置尾随逗号的格式设置选项。 Inspection ID: JSLastCommaInArrayLiteral", + "markdown": "报告数组字面量中尾随逗号的用法。\n\n只有将 JavaScript 语言版本设置为 ECMAScript 5.1 时才报告该警告。\n\n尽管此规范允许在数组中使用尾随逗号,但在使用尾随逗号时,某些浏览器可能会抛出错误。\n\n您可以在**代码样式** \\| **JavaScript** 或 **TypeScript** \\| **标点符号**中配置尾随逗号的格式设置选项。\n\nInspection ID: JSLastCommaInArrayLiteral" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSLastCommaInArrayLiteral", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ConditionalExpressionJS", + "shortDescription": { + "text": "条件表达式" + }, + "fullDescription": { + "text": "报告三元条件表达式。 有些编码标准禁止此类表达式, 而是支持显式的 'if' 语句。 Inspection ID: ConditionalExpressionJS", + "markdown": "报告三元条件表达式。 有些编码标准禁止此类表达式, 而是支持显式的 `if` 语句。\n\nInspection ID: ConditionalExpressionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ConditionalExpressionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能不合需要的代码结构", + "index": 21, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ES6ConvertVarToLetConst", + "shortDescription": { + "text": "已使用 'var' 而不是 'let' 或 'const'" + }, + "fullDescription": { + "text": "报告使用 'var' 而不是 'let' 或 'const' 的声明。 'let' 和 'const' 的作用范围都为块,行为更为严格。 建议将所有 'var' 声明替换为 'let' 或 'const' 声明,具体取决于特定值的语义。 为避免引用错误,可将声明移至函数顶部,或放在变量的第一个用法前。 选择'通过‘全部修复’操作保守地转换变量'选项,以防止使用“全部修复”操作时在这些复杂情况下出现任何更改。 Inspection ID: ES6ConvertVarToLetConst", + "markdown": "报告使用 `var` 而不是 `let` 或 `const` 的声明。 \n`let` 和 `const` 的作用范围都为块,行为更为严格。 \n\n建议将所有 `var` 声明替换为 `let` 或 `const` 声明,具体取决于特定值的语义。 为避免引用错误,可将声明移至函数顶部,或放在变量的第一个用法前。 \n选择'通过'全部修复'操作保守地转换变量'选项,以防止使用\"全部修复\"操作时在这些复杂情况下出现任何更改。\n\nInspection ID: ES6ConvertVarToLetConst" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "ES6ConvertVarToLetConst", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/ES2015 迁移协助", + "index": 31, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSReferencingMutableVariableFromClosure", + "shortDescription": { + "text": "从闭包中引用可变变量" + }, + "fullDescription": { + "text": "报告函数中对外部可变变量的访问。 示例: 'for (var i = 1; i <= 3; i++) {\n setTimeout(function() {\n console.log(i); // 不良\n }, 0);\n }' Inspection ID: JSReferencingMutableVariableFromClosure", + "markdown": "报告函数中对外部可变变量的访问。\n\n示例:\n\n\n for (var i = 1; i <= 3; i++) {\n setTimeout(function() {\n console.log(i); // 不良\n }, 0);\n }\n\nInspection ID: JSReferencingMutableVariableFromClosure" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSReferencingMutableVariableFromClosure", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "PointlessBooleanExpressionJS", + "shortDescription": { + "text": "无意义的语句或布尔表达式" + }, + "fullDescription": { + "text": "报告无意义或复杂的没有意义的布尔表达式或语句。 示例: 'let a = !(false && x);\n let b = false || x;' 应用该快速修复后,结果将如下所示: 'let a = true;\n let b = x;' Inspection ID: PointlessBooleanExpressionJS", + "markdown": "报告无意义或复杂的没有意义的布尔表达式或语句。\n\n示例:\n\n\n let a = !(false && x);\n let b = false || x;\n\n应用该快速修复后,结果将如下所示:\n\n\n let a = true;\n let b = x;\n\nInspection ID: PointlessBooleanExpressionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "PointlessBooleanExpressionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/控制流问题", + "index": 1, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "DynamicallyGeneratedCodeJS", + "shortDescription": { + "text": "执行动态生成的代码" + }, + "fullDescription": { + "text": "报告 'eval()'、'setTimeout()' 或 'setInterval()' 函数的调用或 'Function' 对象的分配。 这些函数用于执行通常动态生成的 任意 JavaScript 文本字符串。 这可能非常令人困惑,并且可能存在安全风险。 忽略静态地向这些方法提供回调函数而不生成代码的情况。 Inspection ID: DynamicallyGeneratedCodeJS", + "markdown": "报告 `eval()`、`setTimeout()` 或 `setInterval()` 函数的调用或 `Function` 对象的分配。 这些函数用于执行通常动态生成的 任意 JavaScript 文本字符串。 这可能非常令人困惑,并且可能存在安全风险。 \n\n忽略静态地向这些方法提供回调函数而不生成代码的情况。\n\nInspection ID: DynamicallyGeneratedCodeJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "DynamicallyGeneratedCodeJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Security" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能引起混淆的代码结构", + "index": 39, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "NegatedConditionalExpressionJS", + "shortDescription": { + "text": "否定条件表达式" + }, + "fullDescription": { + "text": "报告条件被否定的条件表达式。 建议翻转条件表达式中分支的顺序以增加语句的清晰度。 示例:'!condition ? 2 : 1' Inspection ID: NegatedConditionalExpressionJS", + "markdown": "报告条件被否定的条件表达式。 建议翻转条件表达式中分支的顺序以增加语句的清晰度。 示例:`!condition ? 2 : 1`\n\nInspection ID: NegatedConditionalExpressionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "NegatedConditionalExpressionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能引起混淆的代码结构", + "index": 39, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSUrlImportUsage", + "shortDescription": { + "text": "使用了 URL 导入" + }, + "fullDescription": { + "text": "检查 JavaScript 语言中使用过的 URL import。 建议下载指定的远程 URL 的模块。 此类关联使 IDE 可以提供正确的代码补全和导航。 只有 JavaScript 语言中的 ECMAScript 模块支持 import 说明符中的 URL。 Inspection ID: JSUrlImportUsage", + "markdown": "检查 JavaScript 语言中使用过的 URL import。 建议下载指定的远程 URL 的模块。 此类关联使 IDE 可以提供正确的代码补全和导航。 \n\n只有 JavaScript 语言中的 ECMAScript 模块支持 import 说明符中的 URL。\n\nInspection ID: JSUrlImportUsage" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSUrlImportUsage", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Import 和依赖项", + "index": 55, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "UnnecessaryLabelOnContinueStatementJS", + "shortDescription": { + "text": "'continue' 语句上的不必要标签" + }, + "fullDescription": { + "text": "报告可以在不更改控制流的情况下移除其标签的 'continue' 标签语句。 Inspection ID: UnnecessaryLabelOnContinueStatementJS", + "markdown": "报告可以在不更改控制流的情况下移除其标签的 `continue` 标签语句。\n\nInspection ID: UnnecessaryLabelOnContinueStatementJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "UnnecessaryLabelOnContinueStatementJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/控制流问题", + "index": 1, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ChainedEqualityJS", + "shortDescription": { + "text": "链式相等" + }, + "fullDescription": { + "text": "报告链式相等比较(即 'a==b==c')。 此类比较令人困惑。 Inspection ID: ChainedEqualityJS", + "markdown": "报告链式相等比较(即 `a==b==c`)。 此类比较令人困惑。\n\nInspection ID: ChainedEqualityJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ChainedEqualityComparisonsJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/代码样式问题", + "index": 19, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SillyAssignmentJS", + "shortDescription": { + "text": "变量被赋值给自己" + }, + "fullDescription": { + "text": "报告 'x = x' 形式的赋值。 Inspection ID: SillyAssignmentJS", + "markdown": "报告 `x = x` 形式的赋值。\n\nInspection ID: SillyAssignmentJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SillyAssignmentJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/赋值问题", + "index": 48, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSPotentiallyInvalidTargetOfIndexedPropertyAccess", + "shortDescription": { + "text": "索引的属性访问的目标可能不正确" + }, + "fullDescription": { + "text": "报告可能无效的索引属性访问,例如 'Array[1]'。 Inspection ID: JSPotentiallyInvalidTargetOfIndexedPropertyAccess", + "markdown": "报告可能无效的索引属性访问,例如 `Array[1]`。\n\nInspection ID: JSPotentiallyInvalidTargetOfIndexedPropertyAccess" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSPotentiallyInvalidTargetOfIndexedPropertyAccess", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能的 bug", + "index": 26, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSAccessibilityCheck", + "shortDescription": { + "text": "已引用无法访问的 @private 和 @protected 成员" + }, + "fullDescription": { + "text": "报告对使用 '@private' 或 '@protected' 标记进行标记,但不符合这些标记所隐含的可见性规则的 JavaScript 成员的引用。 Inspection ID: JSAccessibilityCheck", + "markdown": "报告对使用 `@private` 或 `@protected` 标记进行标记,但不符合这些标记所隐含的可见性规则的 JavaScript 成员的引用。\n\nInspection ID: JSAccessibilityCheck" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSAccessibilityCheck", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ES6ConvertRequireIntoImport", + "shortDescription": { + "text": "使用了 'require()' 而不是 'import'" + }, + "fullDescription": { + "text": "报告 'require()' 语句。 建议使用 'import' 语句将其转换为 'require()' 调用。 启用“使用‘全部修复’操作转换内部范围中的 require()”, 在使用“全部修复”操作时转换嵌套函数和语句内部的所有 'require()' 调用。 请注意,将内部范围中的 'require()' 语句转换为 'import' 语句 可能会导致代码语义更改。 Import 语句是 static 模块依赖项,它们获得了提升, 这表明它们被移至当前模块的顶部。 'require()' 调用动态加载模块。 它们可以有条件地执行,其范围由使用它们的表达式来定义。 清除“使用‘全部修复’操作转换内部范围中的 require()”复选框,以防止在使用“全部修复”操作时对这些复杂情况进行任何更改。 Inspection ID: ES6ConvertRequireIntoImport", + "markdown": "报告 `require()` 语句。 建议使用 `import` 语句将其转换为 `require()` 调用。 \n\n启用\"使用'全部修复'操作转换内部范围中的 require()\", 在使用\"全部修复\"操作时转换嵌套函数和语句内部的所有 `require()` 调用。 \n\n请注意,将内部范围中的 `require()` 语句转换为 `import` 语句 可能会导致代码语义更改。 Import 语句是 static 模块依赖项,它们获得了提升, 这表明它们被移至当前模块的顶部。 `require()` 调用动态加载模块。 它们可以有条件地执行,其范围由使用它们的表达式来定义。 \n清除\"使用'全部修复'操作转换内部范围中的 require()\"复选框,以防止在使用\"全部修复\"操作时对这些复杂情况进行任何更改。\n\nInspection ID: ES6ConvertRequireIntoImport" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "ES6ConvertRequireIntoImport", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/ES2015 迁移协助", + "index": 31, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "FunctionWithMultipleLoopsJS", + "shortDescription": { + "text": "函数具有多个循环" + }, + "fullDescription": { + "text": "报告具有多个循环语句的函数。 Inspection ID: FunctionWithMultipleLoopsJS", + "markdown": "报告具有多个循环语句的函数。\n\nInspection ID: FunctionWithMultipleLoopsJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "FunctionWithMultipleLoopsJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/函数指标", + "index": 45, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "LabeledStatementJS", + "shortDescription": { + "text": "标记语句" + }, + "fullDescription": { + "text": "报告标签语句。 Inspection ID: LabeledStatementJS", + "markdown": "报告标签语句。\n\nInspection ID: LabeledStatementJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "LabeledStatementJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能不合需要的代码结构", + "index": 21, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "NpmUsedModulesInstalled", + "shortDescription": { + "text": "缺少模块依赖关系" + }, + "fullDescription": { + "text": "报告未安装或未在 package.json 依赖项中列出的 'require()' 调用或 'import' 语句中的模块。 建议安装此模块和/或将其包含到 package.json 中。 对于 'require()' 调用,仅在Node.js Core JavaScript 库范围内的文件中起作用。 Inspection ID: NpmUsedModulesInstalled", + "markdown": "报告未安装或未在 package.json 依赖项中列出的 `require()` 调用或 `import` 语句中的模块。\n\n建议安装此模块和/或将其包含到 package.json 中。\n\n对于 `require()` 调用,仅在*Node.js Core* JavaScript 库范围内的文件中起作用。\n\nInspection ID: NpmUsedModulesInstalled" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "NpmUsedModulesInstalled", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Import 和依赖项", + "index": 55, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "UnusedCatchParameterJS", + "shortDescription": { + "text": "未使用的 'catch' 形参" + }, + "fullDescription": { + "text": "报告未在相应的块中使用的 'catch' 形参。 将忽略名称为 'ignore' 或 'ignored' 的 'catch' 形参。 使用下面的复选框对包含注释的 'catch' 块禁用此检查。 Inspection ID: UnusedCatchParameterJS", + "markdown": "报告未在相应的块中使用的 `catch` 形参。 将忽略名称为 `ignore` 或 `ignored` 的 `catch` 形参。\n\n\n使用下面的复选框对包含注释的\n`catch` 块禁用此检查。\n\nInspection ID: UnusedCatchParameterJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "UnusedCatchParameterJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Try 语句问题", + "index": 43, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "WithStatementJS", + "shortDescription": { + "text": "'with' 语句" + }, + "fullDescription": { + "text": "报告 'with' 语句。 此类语句会导致可能令人困惑的隐式绑定,并且在设置新变量时可能会表现出奇怪的行为。 Inspection ID: WithStatementJS", + "markdown": "报告 `with` 语句。 此类语句会导致可能令人困惑的隐式绑定,并且在设置新变量时可能会表现出奇怪的行为。\n\nInspection ID: WithStatementJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "WithStatementJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能不合需要的代码结构", + "index": 21, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSConstantReassignment", + "shortDescription": { + "text": "尝试分配给常量或只读变量" + }, + "fullDescription": { + "text": "报告将值重新赋给常量或只读变量的情况。 Inspection ID: JSConstantReassignment", + "markdown": "报告将值重新赋给常量或只读变量的情况。\n\nInspection ID: JSConstantReassignment" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "JSConstantReassignment", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/有效性问题", + "index": 36, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TypeScriptCheckImport", + "shortDescription": { + "text": "未解析的已导入名称" + }, + "fullDescription": { + "text": "报告 TypeScript 代码的 'import' 声明中未解析的名称或绑定。 Inspection ID: TypeScriptCheckImport", + "markdown": "报告 TypeScript 代码的 `import` 声明中未解析的名称或绑定。\n\nInspection ID: TypeScriptCheckImport" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "TypeScriptCheckImport", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/TypeScript", + "index": 35, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MagicNumberJS", + "shortDescription": { + "text": "幻数" + }, + "fullDescription": { + "text": "报告使用数值字面量但未通过常量声明来命名的“幻数”。 幻数可能导致生成意图不明确的代码,如果一个代码位置中的幻数被更改,但在另一个代码位置未被更改,则可能导致错误。 数字 0、1、2、3、4、5、6、7、8、9、10、100、1000、 0.0 和 1.0 将被忽略。 Inspection ID: MagicNumberJS", + "markdown": "报告使用数值字面量但未通过常量声明来命名的\"幻数\"。 幻数可能导致生成意图不明确的代码,如果一个代码位置中的幻数被更改,但在另一个代码位置未被更改,则可能导致错误。 数字 0、1、2、3、4、5、6、7、8、9、10、100、1000、 0.0 和 1.0 将被忽略。\n\nInspection ID: MagicNumberJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MagicNumberJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能引起混淆的代码结构", + "index": 39, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "FunctionNamingConventionJS", + "shortDescription": { + "text": "函数命名约定" + }, + "fullDescription": { + "text": "报告名称太短、太长或不符合 指定正则表达式模式的函数。 使用下面提供的字段来指定函数名称的最小长度、 最大长度以及正则表达式。 对正则表达式使用标准的 'java.util.regex' 格式。 Inspection ID: FunctionNamingConventionJS", + "markdown": "报告名称太短、太长或不符合 指定正则表达式模式的函数。\n\n\n使用下面提供的字段来指定函数名称的最小长度、\n最大长度以及正则表达式。 对正则表达式使用标准的 `java.util.regex` 格式。\n\nInspection ID: FunctionNamingConventionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "FunctionNamingConventionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/命名约定", + "index": 58, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSXSyntaxUsed", + "shortDescription": { + "text": "已使用 JSX 语法" + }, + "fullDescription": { + "text": "报告 JavaScript 代码中 JSX 标记的用法。 Inspection ID: JSXSyntaxUsed", + "markdown": "报告 JavaScript 代码中 JSX 标记的用法。\n\nInspection ID: JSXSyntaxUsed" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "JSXSyntaxUsed", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSJoinVariableDeclarationAndAssignment", + "shortDescription": { + "text": "变量声明可与变量的第一个赋值合并" + }, + "fullDescription": { + "text": "报告在没有初始值设定项的情况下声明并且在代码或单个嵌套范围中使用多得多的变量。 建议将此变量移到更靠近其用法的位置,并将其与初始值设定项表达式联接起来。 Inspection ID: JSJoinVariableDeclarationAndAssignment", + "markdown": "报告在没有初始值设定项的情况下声明并且在代码或单个嵌套范围中使用多得多的变量。 建议将此变量移到更靠近其用法的位置,并将其与初始值设定项表达式联接起来。\n\nInspection ID: JSJoinVariableDeclarationAndAssignment" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSJoinVariableDeclarationAndAssignment", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSRedundantSwitchStatement", + "shortDescription": { + "text": "'switch' 语句冗余,可以替换" + }, + "fullDescription": { + "text": "报告语句体为空或仅有一个 'case' 分支或仅有一个 'default' 分支的 'switch' 语句。 Inspection ID: JSRedundantSwitchStatement", + "markdown": "报告语句体为空或仅有一个 `case` 分支或仅有一个 `default` 分支的 `switch` 语句。\n\nInspection ID: JSRedundantSwitchStatement" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSRedundantSwitchStatement", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/switch 语句问题", + "index": 59, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TypeScriptLibrary", + "shortDescription": { + "text": "缺少全局库" + }, + "fullDescription": { + "text": "报告符号必须具备但未在 'tsconfig.json' 中的 'lib' 编译器选项下列出的 TypeScript 库文件。 Inspection ID: TypeScriptLibrary", + "markdown": "报告符号必须具备但未在 `tsconfig.json` 中的 `lib` 编译器选项下列出的 TypeScript 库文件。\n\nInspection ID: TypeScriptLibrary" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "TypeScriptLibrary", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/TypeScript", + "index": 35, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TypeScriptMissingAugmentationImport", + "shortDescription": { + "text": "缺少扩大导入" + }, + "fullDescription": { + "text": "报告没有显式 import 的 增强模块的用法。 Inspection ID: TypeScriptMissingAugmentationImport", + "markdown": "报告没有显式 import 的 [增强模块](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)的用法。\n\nInspection ID: TypeScriptMissingAugmentationImport" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "TypeScriptMissingAugmentationImport", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/TypeScript", + "index": 35, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSUnusedGlobalSymbols", + "shortDescription": { + "text": "未使用的全局符号" + }, + "fullDescription": { + "text": "报告未使用的可以全局访问的 public 函数、变量、类或属性。 Inspection ID: JSUnusedGlobalSymbols", + "markdown": "报告未使用的可以全局访问的 public 函数、变量、类或属性。\n\nInspection ID: JSUnusedGlobalSymbols" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSUnusedGlobalSymbols", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/未使用的符号", + "index": 28, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ES6ConvertModuleExportToExport", + "shortDescription": { + "text": "使用了 'module.exports' 而不是 'export'" + }, + "fullDescription": { + "text": "报告 'module.export' 语句。 建议将其替换为 'export' 或 'export default' 语句。 请注意,用于将 'module.export' 转换为 'export' 的快速修复不适用于函数或语句内部的 'module.export',因为 'export' 语句只能位于模块顶层。 Inspection ID: ES6ConvertModuleExportToExport", + "markdown": "报告 `module.export` 语句。 建议将其替换为 `export` 或 `export default` 语句。 \n\n请注意,用于将 `module.export` 转换为 `export` 的快速修复不适用于函数或语句内部的 `module.export`,因为 `export` 语句只能位于模块顶层。\n\nInspection ID: ES6ConvertModuleExportToExport" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "ES6ConvertModuleExportToExport", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/ES2015 迁移协助", + "index": 31, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "DocumentWriteJS", + "shortDescription": { + "text": "调用 'document.write()'" + }, + "fullDescription": { + "text": "报告对 'document.write()' 或 'document.writeln()' 的方法调用。 在采用显式 DOM 调用(例如 'getElementByID()' 和 'createElement()')时, 此类调用的大多数用法执行效果更好。 此外,'write()' 和 'writeln()' 调用不适用于 XML DOM,包括用于 XHTML 的 DOM(如果以 XML 格式查看)。 这会导致难以指出错误。 Inspection ID: DocumentWriteJS", + "markdown": "报告对 `document.write()` 或 `document.writeln()` 的方法调用。 在采用显式 DOM 调用(例如 `getElementByID()` 和 `createElement()`)时, 此类调用的大多数用法执行效果更好。 此外,`write()` 和 `writeln()` 调用不适用于 XML DOM,包括用于 XHTML 的 DOM(如果以 XML 格式查看)。 这会导致难以指出错误。\n\nInspection ID: DocumentWriteJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "DocumentWriteJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/DOM 问题", + "index": 60, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AnonymousFunctionJS", + "shortDescription": { + "text": "匿名函数" + }, + "fullDescription": { + "text": "报告匿名函数。 函数表达式的显式名称可能有助于调试。 如果 ECMAScript 6 标准中指定了 'name' 属性,则忽略没有名称的函数表达式。 例如,不会报告 'var bar = function() {};'。 Inspection ID: AnonymousFunctionJS", + "markdown": "报告匿名函数。 函数表达式的显式名称可能有助于调试。 如果 ECMAScript 6 标准中指定了 `name` 属性,则忽略没有名称的函数表达式。 例如,不会报告 `var bar = function() {};`。\n\nInspection ID: AnonymousFunctionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "AnonymousFunctionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能不合需要的代码结构", + "index": 21, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "BlockStatementJS", + "shortDescription": { + "text": "不必要的块语句" + }, + "fullDescription": { + "text": "报告不用作 'if' 、'for' 、'while' 、'do' 、 'with' 或 'try' 语句体,也不用作函数声明体的块语句。 从 ECMAScript 6 开始,JavaScript 块为 'let' 和 'const' 变量引入了新的范围, 但独立的块语句在与 'var' 变量一起使用时仍然会引起混淆,并导致难以捉摸的错误。 Inspection ID: BlockStatementJS", + "markdown": "报告不用作 `if` 、`for` 、`while` 、`do` 、 `with` 或 `try` 语句体,也不用作函数声明体的块语句。 从 ECMAScript 6 开始,JavaScript 块为 `let` 和 `const` 变量引入了新的范围, 但独立的块语句在与 `var` 变量一起使用时仍然会引起混淆,并导致难以捉摸的错误。\n\nInspection ID: BlockStatementJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "BlockStatementJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能引起混淆的代码结构", + "index": 39, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ObjectAllocationIgnoredJS", + "shortDescription": { + "text": "对象分配的结果已忽略" + }, + "fullDescription": { + "text": "报告所分配对象的结果被忽略的对象分配,例如 'new Error();' 作为不带任何赋值的语句。 此类分配表达式可以指示奇怪的对象初始化策略。 Inspection ID: ObjectAllocationIgnoredJS", + "markdown": "报告所分配对象的结果被忽略的对象分配,例如 `new Error();` 作为不带任何赋值的语句。 此类分配表达式可以指示奇怪的对象初始化策略。\n\nInspection ID: ObjectAllocationIgnoredJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ObjectAllocationIgnored", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能的 bug", + "index": 26, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "InfiniteRecursionJS", + "shortDescription": { + "text": "无限递归" + }, + "fullDescription": { + "text": "报告肯定会无限递归或 抛出异常的函数。 此类函数可能无法正常返回。 Inspection ID: InfiniteRecursionJS", + "markdown": "报告肯定会无限递归或 抛出异常的函数。 此类函数可能无法正常返回。\n\nInspection ID: InfiniteRecursionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "InfiniteRecursionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能的 bug", + "index": 26, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSTypeOfValues", + "shortDescription": { + "text": "包含非标准值的 'typeof' 比较" + }, + "fullDescription": { + "text": "报告 'typeof' 表达式与不是以下标准类型之一的字面量字符串的比较:'undefined'、'object'、'boolean'、'number'、'string'、'function' 或 'symbol'。 此类比较始终返回 'false'。 Inspection ID: JSTypeOfValues", + "markdown": "报告 `typeof` 表达式与不是以下标准类型之一的字面量字符串的比较:`undefined`、`object`、`boolean`、`number`、`string`、`function` 或 `symbol`。 此类比较始终返回 `false`。\n\nInspection ID: JSTypeOfValues" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSTypeOfValues", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能的 bug", + "index": 26, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "NestedConditionalExpressionJS", + "shortDescription": { + "text": "嵌套条件表达式" + }, + "fullDescription": { + "text": "报告另一个三元条件中的三元条件表达式。 此类嵌套条件可能极其令人困惑,最好替换为更显式的条件逻辑。 Inspection ID: NestedConditionalExpressionJS", + "markdown": "报告另一个三元条件中的三元条件表达式。 此类嵌套条件可能极其令人困惑,最好替换为更显式的条件逻辑。\n\nInspection ID: NestedConditionalExpressionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "NestedConditionalExpressionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能引起混淆的代码结构", + "index": 39, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "IncompatibleMaskJS", + "shortDescription": { + "text": "不兼容的按位掩码运算" + }, + "fullDescription": { + "text": "报告确定评估为 'true' 或 'false' 的按位掩码表达式。 表达式的形式为 '(var & constant1) == constant2' 或 '(var | constant1) == constant2',其中 'constant1' 和 'constant2' 是不兼容的位掩码常量。 示例: '// 不兼容的掩码:由于掩码中的最后一个字节为零,\n// 0x1200 是可能的,但 0x1234 不可能\nif ((mask & 0xFF00) == 0x1234) {...}' Inspection ID: IncompatibleMaskJS", + "markdown": "报告确定评估为 `true` 或 `false` 的按位掩码表达式。 表达式的形式为 `(var & constant1) == constant2` 或 `(var | constant1) == constant2`,其中 `constant1` 和 `constant2` 是不兼容的位掩码常量。\n\n示例:\n\n\n // 不兼容的掩码:由于掩码中的最后一个字节为零,\n // 0x1200 是可能的,但 0x1234 不可能\n if ((mask & 0xFF00) == 0x1234) {...}\n\nInspection ID: IncompatibleMaskJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "IncompatibleBitwiseMaskOperation", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/按位运算问题", + "index": 30, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TextLabelInSwitchStatementJS", + "shortDescription": { + "text": "'switch' 语句中的文本标签" + }, + "fullDescription": { + "text": "报告 'switch' 语句内部的标签语句, 这通常是由于拼写错误所致。 示例: 'switch(x)\n {\n case 1:\n case2: //拼写错误!\n case 3:\n break;\n }' Inspection ID: TextLabelInSwitchStatementJS", + "markdown": "报告 `switch` 语句内部的标签语句, 这通常是由于拼写错误所致。\n\n示例:\n\n\n switch(x)\n {\n case 1:\n case2: //拼写错误!\n case 3:\n break;\n }\n\nInspection ID: TextLabelInSwitchStatementJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "TextLabelInSwitchStatementJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/switch 语句问题", + "index": 59, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ES6PossiblyAsyncFunction", + "shortDescription": { + "text": "非异步函数中的 'await'" + }, + "fullDescription": { + "text": "报告可能想要异步但其实缺少 'async' 修饰符的函数中的 'await' 用法。 虽然 'await' 可以用作标识符,但很可能是打算将其用作运算符, 因此应将包含函数设为 'async' 。 Inspection ID: ES6PossiblyAsyncFunction", + "markdown": "报告可能想要异步但其实缺少 `async` 修饰符的函数中的 `await` 用法。 虽然 `await` 可以用作标识符,但很可能是打算将其用作运算符, 因此应将包含函数设为 `async` 。\n\nInspection ID: ES6PossiblyAsyncFunction" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "ES6PossiblyAsyncFunction", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Async 代码和 promise", + "index": 61, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "EmptyCatchBlockJS", + "shortDescription": { + "text": "空 'catch' 块" + }, + "fullDescription": { + "text": "报告空的 'catch' 块。 这表明错误未加处理就被直接忽略。 'catch' 块中的任何注释都会禁止该检查。 Inspection ID: EmptyCatchBlockJS", + "markdown": "报告空的 `catch` 块。 这表明错误未加处理就被直接忽略。 \n\n`catch` 块中的任何注释都会禁止该检查。\n\nInspection ID: EmptyCatchBlockJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "EmptyCatchBlockJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Try 语句问题", + "index": 43, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSHint", + "shortDescription": { + "text": "JSHint" + }, + "fullDescription": { + "text": "报告 JSHint linter 检测到的问题。 Inspection ID: JSHint", + "markdown": "报告 [JSHint](https://jshint.com/) linter 检测到的问题。\n\nInspection ID: JSHint" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "JSHint", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/代码质量工具", + "index": 62, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "Eslint", + "shortDescription": { + "text": "ESLint" + }, + "fullDescription": { + "text": "报告 ESLint linter 检测到的差异。 高亮显示基于 ESLint 配置文件中为每条规则指定的规则严重性。 清除“使用配置文件中的规则严重性”复选框,对所有 ESLint 规则使用该项检查中配置的严重性。 Inspection ID: Eslint", + "markdown": "报告 [ESLint](https://eslint.org) linter 检测到的差异。 \n\n高亮显示基于 [ESLint 配置文件](https://eslint.org/docs/user-guide/configuring)中为每条规则指定的规则严重性。 \n\n清除\"使用配置文件中的规则严重性\"复选框,对所有 ESLint 规则使用该项检查中配置的严重性。\n\nInspection ID: Eslint" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "Eslint", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/代码质量工具", + "index": 62, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSDuplicatedDeclaration", + "shortDescription": { + "text": "重复声明" + }, + "fullDescription": { + "text": "报告一个范围中的多个声明。 Inspection ID: JSDuplicatedDeclaration", + "markdown": "报告一个范围中的多个声明。\n\nInspection ID: JSDuplicatedDeclaration" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSDuplicatedDeclaration", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSEqualityComparisonWithCoercion.TS", + "shortDescription": { + "text": "相等运算符可能导致类型强制" + }, + "fullDescription": { + "text": "报告可能导致意外类型强制的相等运算符的用法。 建议将 '==' 或 '!=' 相等运算符替换为类型安全的 '===' 或 '!==' 运算符。 根据所选的选项,将报告下面的一种情况: '==' 和 '!=' 运算符的所有用法。 除了与 null 比较之外的所有用法。 某些代码样式允许使用 'x == null' 替代 'x === null || x === undefined'。 仅可疑的表达式,例如:'==' 或 '!=' 与 '0'、''''、'null'、'true'、'false' 或 'undefined' 比较。 Inspection ID: JSEqualityComparisonWithCoercion.TS", + "markdown": "报告可能导致意外类型强制的相等运算符的用法。 建议将 `==` 或 `!=` 相等运算符替换为类型安全的 `===` 或 `!==` 运算符。\n\n根据所选的选项,将报告下面的一种情况:\n\n* `==` 和 `!=` 运算符的所有用法。\n* 除了与 null 比较之外的所有用法。 某些代码样式允许使用 `x == null` 替代 `x === null || x === undefined`。\n* 仅可疑的表达式,例如:`==` 或 `!=` 与 `0`、`''`、`null`、`true`、`false` 或 `undefined` 比较。\n\nInspection ID: JSEqualityComparisonWithCoercion.TS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "EqualityComparisonWithCoercionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/TypeScript", + "index": 35, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSOctalInteger", + "shortDescription": { + "text": "八进制整数" + }, + "fullDescription": { + "text": "报告已弃用的前缀为 '0' 而不是 '0o' 的八进制整数字面量。 现代 ECMAScript 代码中不允许使用此类字面量,在 strict 模式下使用它们会导致错误。 要针对 ES5 和 ES3 语言级别强制执行此检查,请选中下面的“对 ES5 代码中过时的八进制字面量发出警告”复选框。 Inspection ID: JSOctalInteger", + "markdown": "报告已弃用的前缀为 `0` 而不是 `0o` 的八进制整数字面量。 \n现代 ECMAScript 代码中不允许使用此类字面量,在 strict 模式下使用它们会导致错误。 \n要针对 ES5 和 ES3 语言级别强制执行此检查,请选中下面的\"对 ES5 代码中过时的八进制字面量发出警告\"复选框。\n\nInspection ID: JSOctalInteger" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "JSOctalInteger", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/有效性问题", + "index": 36, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ExceptionCaughtLocallyJS", + "shortDescription": { + "text": "将异常用于本地控制流" + }, + "fullDescription": { + "text": "报告异常始终可以通过包含 'try' 语句捕获的 'throw' 语句。 使用 'throw' 语句作为 'goto' 来更改局部控制流令人困惑。 Inspection ID: ExceptionCaughtLocallyJS", + "markdown": "报告异常始终可以通过包含 `try` 语句捕获的 `throw` 语句。 使用 `throw` 语句作为 `goto` 来更改局部控制流令人困惑。\n\nInspection ID: ExceptionCaughtLocallyJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ExceptionCaughtLocallyJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Try 语句问题", + "index": 43, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ThrowFromFinallyBlockJS", + "shortDescription": { + "text": "'throw' 位于 'finally' 块内" + }, + "fullDescription": { + "text": "报告 'finally' 块中的 'throw' 语句。 此类 'throw' 语句可能会掩盖抛出的异常,并导致调试复杂化。 Inspection ID: ThrowFromFinallyBlockJS", + "markdown": "报告 `finally` 块中的 `throw` 语句。 此类 `throw` 语句可能会掩盖抛出的异常,并导致调试复杂化。\n\nInspection ID: ThrowFromFinallyBlockJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ThrowInsideFinallyBlockJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Try 语句问题", + "index": 43, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TypeScriptValidateGenericTypes", + "shortDescription": { + "text": "不正确的泛型实参" + }, + "fullDescription": { + "text": "报告函数、接口或类声明中的无效类型实参。 Inspection ID: TypeScriptValidateGenericTypes", + "markdown": "报告函数、接口或类声明中的无效类型实参。\n\nInspection ID: TypeScriptValidateGenericTypes" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "TypeScriptValidateGenericTypes", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/TypeScript", + "index": 35, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CyclomaticComplexityJS", + "shortDescription": { + "text": "函数过于复杂" + }, + "fullDescription": { + "text": "报告函数中分支点太多的函数(循环复杂度太高)。 此类函数可能令人困惑,也难以测试。 使用下面提供的字段可指定函数的最大可接受循环复杂度。 Inspection ID: CyclomaticComplexityJS", + "markdown": "报告函数中分支点太多的函数(循环复杂度太高)。 此类函数可能令人困惑,也难以测试。\n\n\n使用下面提供的字段可指定函数的最大可接受循环复杂度。\n\nInspection ID: CyclomaticComplexityJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "OverlyComplexFunctionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/函数指标", + "index": 45, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSMismatchedCollectionQueryUpdate", + "shortDescription": { + "text": "不匹配的集合查询和更新" + }, + "fullDescription": { + "text": "报告查询了其内容但未更新或更新了其内容但未查询 的字段或变量的集合。 这种不匹配的查询和更新毫无意义, 并且可能表明有死码或排版错误。 根据查询方法是否返回某些内容或向其传递回调,来自动检测查询方法。 使用下表指定哪些方法是更新方法。 Inspection ID: JSMismatchedCollectionQueryUpdate", + "markdown": "报告查询了其内容但未更新或更新了其内容但未查询 的字段或变量的集合。 这种不匹配的查询和更新毫无意义, 并且可能表明有死码或排版错误。\n\n\n根据查询方法是否返回某些内容或向其传递回调,来自动检测查询方法。\n使用下表指定哪些方法是更新方法。\n\nInspection ID: JSMismatchedCollectionQueryUpdate" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSMismatchedCollectionQueryUpdate", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "PackageJsonMismatchedDependency", + "shortDescription": { + "text": "package.json 中的依赖关系不匹配" + }, + "fullDescription": { + "text": "报告未安装或与指定的版本范围不匹配的 package.json 中的依赖项。 Inspection ID: PackageJsonMismatchedDependency", + "markdown": "报告未安装或与指定的[版本范围](https://docs.npmjs.com/about-semantic-versioning)不匹配的 package.json 中的依赖项。\n\nInspection ID: PackageJsonMismatchedDependency" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "PackageJsonMismatchedDependency", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Import 和依赖项", + "index": 55, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSPotentiallyInvalidUsageOfThis", + "shortDescription": { + "text": "从闭包对 'this' 的引用可能无效" + }, + "fullDescription": { + "text": "报告闭包中的用于引用外部上下文属性的 'this'。 示例: 'function Outer() {\n this.outerProp = 1;\n function inner() {\n // 不良,因为 Outer 的 'outerProp' \n // 不会如预期的那样\n // 在调用 'new Outer()' 时在此处进行更新\n this.outerProp = 2;\n }\n inner();\n}' Inspection ID: JSPotentiallyInvalidUsageOfThis", + "markdown": "报告闭包中的用于引用外部上下文属性的 `this`。\n\n示例:\n\n\n function Outer() {\n this.outerProp = 1;\n function inner() {\n // 不良,因为 Outer 的 'outerProp' \n // 不会如预期的那样\n // 在调用 'new Outer()' 时在此处进行更新\n this.outerProp = 2;\n }\n inner();\n }\n\nInspection ID: JSPotentiallyInvalidUsageOfThis" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSPotentiallyInvalidUsageOfThis", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能的 bug", + "index": 26, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSMissingSwitchDefault", + "shortDescription": { + "text": "'switch' 语句没有 'default' 分支" + }, + "fullDescription": { + "text": "报告在未枚举某些可能值的情况下不含 'default' 子句的 'switch' 语句。 Inspection ID: JSMissingSwitchDefault", + "markdown": "报告在未枚举某些可能值的情况下不含 `default` 子句的 `switch` 语句。\n\nInspection ID: JSMissingSwitchDefault" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSMissingSwitchDefault", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/switch 语句问题", + "index": 59, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSXNamespaceValidation", + "shortDescription": { + "text": "缺少 JSX 命名空间" + }, + "fullDescription": { + "text": "报告未导入命名空间的 JSX 构造的用法。 将命名空间置于文件范围中可以确保正确编译代码。 Inspection ID: JSXNamespaceValidation", + "markdown": "报告未导入命名空间的 JSX 构造的用法。 将命名空间置于文件范围中可以确保正确编译代码。\n\nInspection ID: JSXNamespaceValidation" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSXNamespaceValidation", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Import 和依赖项", + "index": 55, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSUnresolvedLibraryURL", + "shortDescription": { + "text": "HTTP 链接缺少本地存储的库" + }, + "fullDescription": { + "text": "报告未与任何本地存储文件关联的外部 JavaScript 库的 URL。 建议下载此库。 此类关联使 IDE 可以提供正确的代码补全和导航。 Inspection ID: JSUnresolvedLibraryURL", + "markdown": "报告未与任何本地存储文件关联的外部 JavaScript 库的 URL。 建议下载此库。 此类关联使 IDE 可以提供正确的代码补全和导航。\n\nInspection ID: JSUnresolvedLibraryURL" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSUnresolvedLibraryURL", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ES6PreferShortImport", + "shortDescription": { + "text": "可以缩短导入" + }, + "fullDescription": { + "text": "报告可缩短其 'from' 部分的 ES6 import。 建议导入父目录。 Inspection ID: ES6PreferShortImport", + "markdown": "报告可缩短其 `from` 部分的 ES6 import。 建议导入父目录。\n\nInspection ID: ES6PreferShortImport" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ES6PreferShortImport", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "PointlessBitwiseExpressionJS", + "shortDescription": { + "text": "可以简化按位表达式" + }, + "fullDescription": { + "text": "报告包含 'and' 零、 'or' 零或零移位的表达式。 此类表达式可能是由于没有完全完成自动化重构而造成。 Inspection ID: PointlessBitwiseExpressionJS", + "markdown": "报告包含 `and` 零、 ` or` 零或零移位的表达式。 此类表达式可能是由于没有完全完成自动化重构而造成。\n\nInspection ID: PointlessBitwiseExpressionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "PointlessBitwiseExpressionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/按位运算问题", + "index": 30, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "InfiniteLoopJS", + "shortDescription": { + "text": "无限循环语句" + }, + "fullDescription": { + "text": "报告只能通过抛出异常来退出的 'for' 、 'while' 或 'do' 语句。 此类语句通常表示编码错误。 Inspection ID: InfiniteLoopJS", + "markdown": "报告只能通过抛出异常来退出的 `for` 、 `while` 或 `do` 语句。 此类语句通常表示编码错误。\n\nInspection ID: InfiniteLoopJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "InfiniteLoopJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能的 bug", + "index": 26, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSStringConcatenationToES6Template", + "shortDescription": { + "text": "已使用字符串串联而不是模板字面量" + }, + "fullDescription": { + "text": "报告字符串串联。 建议将其替换为模板字面量。 示例 '\"result: \" + a + \".\"' 应用快速修复后,代码如下所示: '`result: ${a}.`' Inspection ID: JSStringConcatenationToES6Template", + "markdown": "报告字符串串联。 建议将其替换为[模板字面量](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)。\n\n示例\n\n \"result: \" + a + \".\" \n\n应用快速修复后,代码如下所示:\n\n `result: ${a}.` \n\nInspection ID: JSStringConcatenationToES6Template" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSStringConcatenationToES6Template", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/ES2015 迁移协助", + "index": 31, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSArrowFunctionBracesCanBeRemoved", + "shortDescription": { + "text": "箭头函数体周围的大括号冗余" + }, + "fullDescription": { + "text": "报告其函数体仅由大括号和恰好一条语句组成的箭头函数。 建议转换为不带大括号的简洁语法。 'let incrementer = (x) => {return x + 1};' 应用快速修复后,代码段如下所示: 'let incrementer = (x) => x + 1;' Inspection ID: JSArrowFunctionBracesCanBeRemoved", + "markdown": "报告其函数体仅由大括号和恰好一条语句组成的箭头函数。 建议转换为不带大括号的简洁语法。\n\n\n let incrementer = (x) => {return x + 1};\n\n应用快速修复后,代码段如下所示:\n\n\n let incrementer = (x) => x + 1;\n\nInspection ID: JSArrowFunctionBracesCanBeRemoved" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSArrowFunctionBracesCanBeRemoved", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/代码样式问题", + "index": 19, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ReplaceAssignmentWithOperatorAssignmentJS", + "shortDescription": { + "text": "赋值可被替换为运算符赋值" + }, + "fullDescription": { + "text": "报告可由运算符赋值替换的赋值运算,以使代码更简洁、更清晰。 示例: 'x = x + 3;'\n 'x = x / 3;'\n 应用该快速修复后,结果将如下所示: 'x += 3;'\n 'x /= 3;'\n Inspection ID: ReplaceAssignmentWithOperatorAssignmentJS", + "markdown": "报告可由运算符赋值替换的赋值运算,以使代码更简洁、更清晰。\n\n\n示例:\n\n x = x + 3;\n x = x / 3;\n\n应用该快速修复后,结果将如下所示:\n\n x += 3;\n x /= 3;\n\nInspection ID: ReplaceAssignmentWithOperatorAssignmentJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "AssignmentReplaceableWithOperatorAssignmentJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/赋值问题", + "index": 48, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSFileReferences", + "shortDescription": { + "text": "未解析的文件引用" + }, + "fullDescription": { + "text": "报告 JavaScript 文件中未解析的文件引用,包括 CommonJS 和 AMD 模块引用。 Inspection ID: JSFileReferences", + "markdown": "报告 JavaScript 文件中未解析的文件引用,包括 CommonJS 和 AMD 模块引用。\n\nInspection ID: JSFileReferences" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSFileReferences", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "FunctionWithInconsistentReturnsJS", + "shortDescription": { + "text": "函数的返回不一致" + }, + "fullDescription": { + "text": "报告在某些情况下返回值但在其他情况下不返回值的函数。 这通常表明有错误。 示例: 'function foo() {\n if (true)\n return 3;\n return;\n}'\n Inspection ID: FunctionWithInconsistentReturnsJS", + "markdown": "报告在某些情况下返回值但在其他情况下不返回值的函数。 这通常表明有错误。\n\n示例:\n\n\n function foo() {\n if (true)\n return 3;\n return;\n }\n\nInspection ID: FunctionWithInconsistentReturnsJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "FunctionWithInconsistentReturnsJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/有效性问题", + "index": 36, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ES6ClassMemberInitializationOrder", + "shortDescription": { + "text": "在 static 初始值设定项中使用可能未分配的属性" + }, + "fullDescription": { + "text": "报告引用另一个未提升的类成员,而被引用的这个类成员可能尚未初始化的类成员初始值设定项。 由于针对字段进行类成员初始化,所以一个字段不能引用后面声明的另一个字段。 Inspection ID: ES6ClassMemberInitializationOrder", + "markdown": "报告引用另一个未提升的类成员,而被引用的这个类成员可能尚未初始化的类成员初始值设定项。 \n\n由于针对字段进行类成员初始化,所以一个字段不能引用后面声明的另一个字段。\n\nInspection ID: ES6ClassMemberInitializationOrder" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ES6ClassMemberInitializationOrder", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "EmptyTryBlockJS", + "shortDescription": { + "text": "空 'try' 块" + }, + "fullDescription": { + "text": "报告空的 'try' 块,此类块通常表示有错误。 Inspection ID: EmptyTryBlockJS", + "markdown": "报告空的 `try` 块,此类块通常表示有错误。\n\nInspection ID: EmptyTryBlockJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "EmptyTryBlockJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Try 语句问题", + "index": 43, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ReservedWordUsedAsNameJS", + "shortDescription": { + "text": "保留词用作名称" + }, + "fullDescription": { + "text": "报告用作名称的 JavaScript 保留字。 JavaScript 规范 保留了许多当前未用作关键字的词语。 如果后续版本的 JavaScript 开始使用这些词语作为关键字, 使用这些词语作为标识符可能会破坏代码。 Inspection ID: ReservedWordUsedAsNameJS", + "markdown": "报告用作名称的 JavaScript 保留字。 JavaScript 规范 保留了许多当前未用作关键字的词语。 如果后续版本的 JavaScript 开始使用这些词语作为关键字, 使用这些词语作为标识符可能会破坏代码。\n\nInspection ID: ReservedWordUsedAsNameJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ReservedWordAsName", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/有效性问题", + "index": 36, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSClassNamingConvention", + "shortDescription": { + "text": "类命名约定" + }, + "fullDescription": { + "text": "报告用 JSDoc '@constructor' 或 '@class' 标记进行注解, 并且其名称太短、太长或不符合指定正则表达式模式的类或函数。 使用下面提供的字段来指定类名称的最小长度、 最大长度以及预期的正则表达式。 对正则表达式使用标准的 'java.util.regex' 格式。 Inspection ID: JSClassNamingConvention", + "markdown": "报告用 JSDoc `@constructor` 或 `@class` 标记进行注解, 并且其名称太短、太长或不符合指定正则表达式模式的类或函数。\n\n\n使用下面提供的字段来指定类名称的最小长度、\n最大长度以及预期的正则表达式。 对正则表达式使用标准的 `java.util.regex` 格式。\n\nInspection ID: JSClassNamingConvention" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSClassNamingConvention", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/命名约定", + "index": 58, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "NestedFunctionJS", + "shortDescription": { + "text": "嵌套函数" + }, + "fullDescription": { + "text": "报告嵌套在另一个函数中的函数。 尽管 JavaScript 允许嵌套函数,但此类结构可能令人困惑。 使用下面的复选框以忽略匿名嵌套函数。 Inspection ID: NestedFunctionJS", + "markdown": "报告嵌套在另一个函数中的函数。 尽管 JavaScript 允许嵌套函数,但此类结构可能令人困惑。\n\n\n使用下面的复选框以忽略匿名嵌套函数。\n\nInspection ID: NestedFunctionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "NestedFunctionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能引起混淆的代码结构", + "index": 39, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XHTMLIncompatabilitiesJS", + "shortDescription": { + "text": "不兼容的 XHTML 用法" + }, + "fullDescription": { + "text": "报告可能会导致 XHTML 文档出现问题的 常见 JavaScript DOM 模式。 特别是,根据文档是作为 XML 还是 HTML 加载, 所检测到的模式的行为将完全不同。 这可能导致难以捉摸的错误,即脚本行为依赖于文档的 MIME 类型而不是依赖于其内容。 检测到的模式包括 document.body、 document.images、 document.applets、 document.links、 document.forms 和 document.anchors。 Inspection ID: XHTMLIncompatabilitiesJS", + "markdown": "报告可能会导致 XHTML 文档出现问题的 常见 JavaScript DOM 模式。 特别是,根据文档是作为 XML 还是 HTML 加载, 所检测到的模式的行为将完全不同。 这可能导致难以捉摸的错误,即脚本行为依赖于文档的 MIME 类型而不是依赖于其内容。 检测到的模式包括 **document.body** 、 **document.images** 、 **document.applets** 、 **document.links** 、 **document.forms** 和 **document.anchors** 。\n\nInspection ID: XHTMLIncompatabilitiesJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "XHTMLIncompatabilitiesJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/DOM 问题", + "index": 60, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "IncrementDecrementResultUsedJS", + "shortDescription": { + "text": "使用增量或减量的结果" + }, + "fullDescription": { + "text": "报告赋值结果在包含其的表达式中 使用的递增 ('++') 或递减 ('--') 表达式。 此类赋值可能会由于运算顺序而导致困惑,因为赋值的评估可能会以意想不到的方式影响外部表达式。 示例:'var a = b++' Inspection ID: IncrementDecrementResultUsedJS", + "markdown": "报告赋值结果在包含其的表达式中 使用的递增 (`++`) 或递减 (`--`) 表达式。 此类赋值可能会由于运算顺序而导致困惑,因为赋值的评估可能会以意想不到的方式影响外部表达式。 示例:`var a = b++`\n\nInspection ID: IncrementDecrementResultUsedJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "IncrementDecrementResultUsedJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能引起混淆的代码结构", + "index": 39, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SuspiciousTypeOfGuard", + "shortDescription": { + "text": "类型 guard 检查不健全" + }, + "fullDescription": { + "text": "报告 'typeof' 或 'instanceof' 不健全的类型防护检查。 在以下两种情况下,'typeof x' 类型防护可能不健全: 'typeof x' 不对应于指定的值(例如,当 'x' 为 'string | boolean' 类型时,'typeof x === 'number'') 'typeof x' 始终对应于指定的值(例如,当 'x' 为 'string' 类型时,'typeof x === 'string'') 在以下两种情况下,'x instanceof A' 类型防护可能不健全: 'x' 的类型与 'A' 不相关 'x' 的类型是 'A' 或 'A' 的子类型 Inspection ID: SuspiciousTypeOfGuard", + "markdown": "报告 `typeof` 或 `instanceof` 不健全的类型防护检查。 在以下两种情况下,`typeof x` 类型防护可能不健全:\n\n* `typeof x` 不对应于指定的值(例如,当 `x` 为 'string \\| boolean' 类型时,`typeof x === 'number'`)\n* `typeof x` 始终对应于指定的值(例如,当 `x` 为 'string' 类型时,`typeof x === 'string'`)\n\n在以下两种情况下,`x instanceof A` 类型防护可能不健全:\n\n* `x` 的类型与 `A` 不相关\n* `x` 的类型是 `A` 或 `A` 的子类型\n\nInspection ID: SuspiciousTypeOfGuard" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SuspiciousTypeOfGuard", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/控制流问题", + "index": 1, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TypeScriptJSXUnresolvedComponent", + "shortDescription": { + "text": "未解析的 JSX 组件" + }, + "fullDescription": { + "text": "报告对 JSX 组件的未解析引用。 如果引用的组件在此项目或其依赖项中定义,则建议添加 import 语句,或使用指定名称创建新组件。 新组件的模板可以在“编辑器 | 文件和代码模板”中修改。 Inspection ID: TypeScriptJSXUnresolvedComponent", + "markdown": "报告对 JSX 组件的未解析引用。 如果引用的组件在此项目或其依赖项中定义,则建议添加 import 语句,或使用指定名称创建新组件。\n\n新组件的模板可以在\"编辑器 \\| 文件和代码模板\"中修改。\n\nInspection ID: TypeScriptJSXUnresolvedComponent" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "TypeScriptJSXUnresolvedComponent", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/TypeScript", + "index": 35, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TypeScriptFieldCanBeMadeReadonly", + "shortDescription": { + "text": "可以为只读字段" + }, + "fullDescription": { + "text": "报告可设为 readonly 的 private 字段(例如,如果该字段仅在构造函数中赋值)。 Inspection ID: TypeScriptFieldCanBeMadeReadonly", + "markdown": "报告可设为 readonly 的 private 字段(例如,如果该字段仅在构造函数中赋值)。\n\nInspection ID: TypeScriptFieldCanBeMadeReadonly" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "TypeScriptFieldCanBeMadeReadonly", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/TypeScript", + "index": 35, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ES6DestructuringVariablesMerge", + "shortDescription": { + "text": "正在析构具有相同键的属性" + }, + "fullDescription": { + "text": "报告多个包含相同键的解构属性。 建议合并这些属性。 Inspection ID: ES6DestructuringVariablesMerge", + "markdown": "报告多个包含相同键的解构属性。 建议合并这些属性。\n\nInspection ID: ES6DestructuringVariablesMerge" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "ES6DestructuringVariablesMerge", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "LoopStatementThatDoesntLoopJS", + "shortDescription": { + "text": "不循环的循环语句" + }, + "fullDescription": { + "text": "报告语句体最多可以执行一次的 'for' 、 'while' 或 'do' 语句。 这通常表示有错误。 Inspection ID: LoopStatementThatDoesntLoopJS", + "markdown": "报告语句体最多可以执行一次的 `for` 、 `while` 或 `do` 语句。 这通常表示有错误。\n\nInspection ID: LoopStatementThatDoesntLoopJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "LoopStatementThatDoesntLoopJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/控制流问题", + "index": 1, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "NegatedIfStatementJS", + "shortDescription": { + "text": "否定的 'if' 语句" + }, + "fullDescription": { + "text": "报告具有 else 分支和否定条件的 if 语句。 翻转 if 和 else 分支的顺序通常会改进此类语句的清晰度。 Inspection ID: NegatedIfStatementJS", + "markdown": "报告具有 **else** 分支和否定条件的 **if** 语句。 翻转 **if** 和 **else** 分支的顺序通常会改进此类语句的清晰度。\n\nInspection ID: NegatedIfStatementJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "NegatedIfStatementJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能引起混淆的代码结构", + "index": 39, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSNonASCIINames", + "shortDescription": { + "text": "包含非 ASCII 符号的标识符" + }, + "fullDescription": { + "text": "报告名称中的非 ASCII 符号。 如果选择“仅允许 ASCII 名称”选项,则报告所有包含非 ASCII 符号的名称。 否则,报告所有同时包含 ASCII 和非 ASCII 符号的名称。 Inspection ID: JSNonASCIINames", + "markdown": "报告名称中的非 ASCII 符号。 \n\n如果选择\"仅允许 ASCII 名称\"选项,则报告所有包含非 ASCII 符号的名称。 \n否则,报告所有同时包含 ASCII 和非 ASCII 符号的名称。\n\nInspection ID: JSNonASCIINames" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSNonASCIINames", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/命名约定", + "index": 58, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TypeScriptRedundantGenericType", + "shortDescription": { + "text": "冗余类型实参" + }, + "fullDescription": { + "text": "报告其值等于默认值并且可以移除的类型实参。 示例: 'type Foo = T;\nlet z: Foo;' Inspection ID: TypeScriptRedundantGenericType", + "markdown": "报告其值等于默认值并且可以移除的类型实参。\n\n\n示例:\n\n\n type Foo = T;\n let z: Foo;\n\nInspection ID: TypeScriptRedundantGenericType" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "TypeScriptRedundantGenericType", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/TypeScript", + "index": 35, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TypeScriptUMDGlobal", + "shortDescription": { + "text": "已引用的 UMD 全局变量" + }, + "fullDescription": { + "text": "如果当前文件是模块(ECMAScript 或 CommonJS),则报告 Universal Module Definition (UMD) 全局变量的用法。 如果没有隐式包含此库,那么在没有显式 import 的情况下引用 UMD 变量可能会导致运行时错误。 Inspection ID: TypeScriptUMDGlobal", + "markdown": "如果当前文件是模块(ECMAScript 或 CommonJS),则报告 Universal Module Definition (UMD) 全局变量的用法。 如果没有隐式包含此库,那么在没有显式 import 的情况下引用 UMD 变量可能会导致运行时错误。\n\nInspection ID: TypeScriptUMDGlobal" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "TypeScriptUMDGlobal", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/TypeScript", + "index": 35, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "UnnecessaryReturnJS", + "shortDescription": { + "text": "不必要的 'return' 语句" + }, + "fullDescription": { + "text": "报告不必要的 'return' 语句,即不返回值并且刚好在函数'贯穿'底部之前发生的 'return' 语句。 这些语句可以安全移除。 Inspection ID: UnnecessaryReturnJS", + "markdown": "报告不必要的 `return` 语句,即不返回值并且刚好在函数'贯穿'底部之前发生的 `return` 语句。 这些语句可以安全移除。\n\nInspection ID: UnnecessaryReturnJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "UnnecessaryReturnStatementJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/控制流问题", + "index": 1, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ConditionalExpressionWithIdenticalBranchesJS", + "shortDescription": { + "text": "具有相同分支的条件表达式" + }, + "fullDescription": { + "text": "报告具有相同的 'then' 和 'else' 分支的三元条件表达式。 Inspection ID: ConditionalExpressionWithIdenticalBranchesJS", + "markdown": "报告具有相同的 `then` 和 `else` 分支的三元条件表达式。\n\nInspection ID: ConditionalExpressionWithIdenticalBranchesJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ConditionalExpressionWithIdenticalBranchesJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/控制流问题", + "index": 1, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSUnfilteredForInLoop", + "shortDescription": { + "text": "未过滤 for..in 循环" + }, + "fullDescription": { + "text": "报告未筛选的 'for-in' 循环。 使用此结构不仅会处理对象自身的属性,还会处理对象原型的属性。 在某些特定情况下可能出人意料,例如,在复制或修改所有属性的实用程序方法中 或者 'Object' 的原型可能被错误修改时。 例如,以下代码将打印 42 和 myMethod: 'Object.prototype.myMethod = function myMethod() {};\nlet a = { foo: 42 };\nfor (let i in a) {\n console.log(a[i]);\n}' 建议将整个循环替换为 'Object.keys()' 方法,或添加 'hasOwnProperty()' 检查。 应用快速修复后,代码如下所示: 'for (let i in a) {\n if (a.hasOwnProperty(i)) {\n console.log(a[i]);\n }\n}' Inspection ID: JSUnfilteredForInLoop", + "markdown": "报告未筛选的 `for-in` 循环。 \n\n使用此结构不仅会处理对象自身的属性,还会处理对象原型的属性。 在某些特定情况下可能出人意料,例如,在复制或修改所有属性的实用程序方法中 或者 `Object` 的原型可能被错误修改时。 例如,以下代码将打印 **42** 和 **myMethod** : \n\n\n Object.prototype.myMethod = function myMethod() {};\n let a = { foo: 42 };\n for (let i in a) {\n console.log(a[i]);\n }\n\n建议将整个循环替换为 `Object.keys()` 方法,或添加 `hasOwnProperty()` 检查。 应用快速修复后,代码如下所示:\n\n\n for (let i in a) {\n if (a.hasOwnProperty(i)) {\n console.log(a[i]);\n }\n }\n\nInspection ID: JSUnfilteredForInLoop" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSUnfilteredForInLoop", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSAnnotator", + "shortDescription": { + "text": "未遵循 ECMAScript 规范" + }, + "fullDescription": { + "text": "报告基本语法问题以及与语言规范不一致的情况,例如关键字的无效用法、不兼容的数字格式的用法或 getter/setter 的多个形参。 通常必须报告此类错误,并且不应禁用这些错误。 但在某些情况下,例如由于 JavaScript 的动态性质而导致问题、使用尚未支持的语言功能或 IDE 检查器中有错误时,禁止报告这些很基本的错误可能会很方便。 Inspection ID: JSAnnotator", + "markdown": "报告基本语法问题以及与语言规范不一致的情况,例如关键字的无效用法、不兼容的数字格式的用法或 getter/setter 的多个形参。 \n通常必须报告此类错误,并且不应禁用这些错误。 但在某些情况下,例如由于 JavaScript 的动态性质而导致问题、使用尚未支持的语言功能或 IDE 检查器中有错误时,禁止报告这些很基本的错误可能会很方便。\n\nInspection ID: JSAnnotator" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "JSAnnotator", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSIncompatibleTypesComparison", + "shortDescription": { + "text": "类型不兼容的表达式的比较" + }, + "fullDescription": { + "text": "报告与不兼容类型的操作数或不含可能公共值的类型的操作数的比较。 Inspection ID: JSIncompatibleTypesComparison", + "markdown": "报告与不兼容类型的操作数或不含可能公共值的类型的操作数的比较。\n\nInspection ID: JSIncompatibleTypesComparison" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSIncompatibleTypesComparison", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能的 bug", + "index": 26, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSLastCommaInObjectLiteral", + "shortDescription": { + "text": "对象字面量中的最后一个逗号多余" + }, + "fullDescription": { + "text": "报告对象字面量中尾随逗号的用法。 只有将 JavaScript 语言版本设置为 ECMAScript 5.1 时才报告该警告。 此规范允许在对象字面量中使用尾随逗号,但在使用尾随逗号时,某些浏览器可能会抛出错误。 您可以在代码样式 | JavaScript 或 TypeScript | 标点符号中配置尾随逗号的格式设置选项。 Inspection ID: JSLastCommaInObjectLiteral", + "markdown": "报告对象字面量中尾随逗号的用法。\n\n只有将 JavaScript 语言版本设置为 ECMAScript 5.1 时才报告该警告。\n\n此规范允许在对象字面量中使用尾随逗号,但在使用尾随逗号时,某些浏览器可能会抛出错误。\n\n您可以在**代码样式** \\| **JavaScript** 或 **TypeScript** \\| **标点符号**中配置尾随逗号的格式设置选项。\n\nInspection ID: JSLastCommaInObjectLiteral" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSLastCommaInObjectLiteral", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSFunctionExpressionToArrowFunction", + "shortDescription": { + "text": "使用了函数表达式而不是箭头函数" + }, + "fullDescription": { + "text": "报告函数表达式。 建议将其转换为箭头函数。 示例: 'arr.map(function(el) {return el + 1})' 应用快速修复后,代码如下所示: 'arr.map(el => el + 1)' Inspection ID: JSFunctionExpressionToArrowFunction", + "markdown": "报告[函数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function)表达式。 建议将其转换为[箭头函数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions)。\n\n示例:\n\n arr.map(function(el) {return el + 1})\n\n应用快速修复后,代码如下所示:\n\n arr.map(el => el + 1)\n\nInspection ID: JSFunctionExpressionToArrowFunction" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSFunctionExpressionToArrowFunction", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/ES2015 迁移协助", + "index": 31, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ES6TopLevelAwaitExpression", + "shortDescription": { + "text": "顶层 'await' 表达式" + }, + "fullDescription": { + "text": "报告顶层 'await' 表达式的用法。 虽然有人在提出新的 'top-level async' 建议,但不允许在 async 函数之外使用 'await'。 Inspection ID: ES6TopLevelAwaitExpression", + "markdown": "报告顶层 `await` 表达式的用法。 虽然有人在提出新的 'top-level async' 建议,但不允许在 async 函数之外使用 `await`。\n\nInspection ID: ES6TopLevelAwaitExpression" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "ES6TopLevelAwaitExpression", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Async 代码和 promise", + "index": 61, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ES6MissingAwait", + "shortDescription": { + "text": "异步函数调用缺少 await" + }, + "fullDescription": { + "text": "报告 'async' 函数中没有预期的 'await' 前缀的 'async' 函数调用。 此类调用返回 'Promise',并且控制流会立即继续。 示例: 'async function bar() { /* ... */ }\nasync function foo() {\n bar(); // 不良\n}' 应用快速修复后,将添加 'await' 前缀: 'async function bar() { /* ... */ }\nasync function foo() {\n await bar(); // 优良\n}' 在选中“报告 return 语句中的 promise”复选框的情况下,还建议在 return 语句中添加 'await'。 虽然一般不必这样做,但它有两大好处。 使用 'try-catch' 环绕代码时,您不会忘记去添加 'await'。 显式的 'await' 有助于 V8 运行时提供异步堆栈跟踪。 Inspection ID: ES6MissingAwait", + "markdown": "报告 `async` 函数中没有预期的 `await` 前缀的 ` async` 函数调用。 此类调用返回 `Promise`,并且控制流会立即继续。\n\n示例:\n\n\n async function bar() { /* ... */ }\n async function foo() {\n bar(); // 不良\n }\n\n\n应用快速修复后,将添加 `await` 前缀:\n\n\n async function bar() { /* ... */ }\n async function foo() {\n await bar(); // 优良\n }\n\n在选中\"报告 return 语句中的 promise\"复选框的情况下,还建议在 return 语句中添加 `await`。 \n虽然一般不必这样做,但它有两大好处。 \n\n* 使用 `try-catch` 环绕代码时,您不会忘记去添加 `await`。\n* 显式的 `await` 有助于 V8 运行时提供[异步堆栈跟踪](https://bit.ly/v8-zero-cost-async-stack-traces)。\n\nInspection ID: ES6MissingAwait" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "ES6MissingAwait", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Async 代码和 promise", + "index": 61, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TailRecursionJS", + "shortDescription": { + "text": "尾递归" + }, + "fullDescription": { + "text": "报告尾递归,即报告函数在返回之前 调用自身作为最后操作的情况。 尾递归总是可以替换为循环,循环的速度快得多。 一些 JavaScript 引擎执行这种优化,而另一些则不执行。 因此,尾递归解决方案在不同环境下 可能具有截然不同的性能特点。 Inspection ID: TailRecursionJS", + "markdown": "报告尾递归,即报告函数在返回之前 调用自身作为最后操作的情况。 尾递归总是可以替换为循环,循环的速度快得多。 一些 JavaScript 引擎执行这种优化,而另一些则不执行。 因此,尾递归解决方案在不同环境下 可能具有截然不同的性能特点。\n\nInspection ID: TailRecursionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "TailRecursionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Performance" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/控制流问题", + "index": 1, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ES6ConvertToForOf", + "shortDescription": { + "text": "使用了 'for..in' 而不是 'for..of'" + }, + "fullDescription": { + "text": "报告对数组的 'for..in' 循环的用法。 建议将其替换为 'for..of' 循环。 'for..of' 循环 (在 ECMAScript 6 中引入) 对 'iterable' 对象进行迭代。 对于数组,此结构比 'for..in' 更可取,因为它只处理数组值,不处理数组对象的属性。 Inspection ID: ES6ConvertToForOf", + "markdown": "报告对数组的 [for..in](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in) 循环的用法。 建议将其替换为 [for..of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of) 循环。 \n`for..of` 循环 (在 ECMAScript 6 中引入) 对 `iterable` 对象进行迭代。 对于数组,此结构比 `for..in` 更可取,因为它只处理数组值,不处理数组对象的属性。\n\nInspection ID: ES6ConvertToForOf" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "ES6ConvertToForOf", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/ES2015 迁移协助", + "index": 31, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ParameterNamingConventionJS", + "shortDescription": { + "text": "函数形参命名约定" + }, + "fullDescription": { + "text": "报告名称太短、太长或不符合 指定正则表达式模式的函数形参。 使用下面提供的字段来指定局部变量名称的最小长度、 最大长度以及预期的正则表达式。 使用标准 'java.util.regex' 格式的正则表达式。 Inspection ID: ParameterNamingConventionJS", + "markdown": "报告名称太短、太长或不符合 指定正则表达式模式的函数形参。\n\n\n使用下面提供的字段来指定局部变量名称的最小长度、\n最大长度以及预期的正则表达式。 使用标准 `java.util.regex` 格式的正则表达式。\n\nInspection ID: ParameterNamingConventionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ParameterNamingConventionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/命名约定", + "index": 58, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSUndefinedPropertyAssignment", + "shortDescription": { + "text": "未定义的属性赋值" + }, + "fullDescription": { + "text": "报告对未在变量类型中定义的属性的赋值。 示例: '/**\n * @type {{ property1: string, property2: number }}\n */\nlet myVariable = create();\n\nmyVariable.newProperty = 3; // 不良' Inspection ID: JSUndefinedPropertyAssignment", + "markdown": "报告对未在变量类型中定义的属性的赋值。\n\n示例:\n\n\n /**\n * @type {{ property1: string, property2: number }}\n */\n let myVariable = create();\n\n myVariable.newProperty = 3; // 不良\n\nInspection ID: JSUndefinedPropertyAssignment" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSUndefinedPropertyAssignment", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/代码样式问题", + "index": 19, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "StandardJS", + "shortDescription": { + "text": "标准代码样式" + }, + "fullDescription": { + "text": "报告 JavaScript Standard Style linter 检测到的差异。 编辑器中高亮显示的严重性基于 linter 报告的严重性级别。 Inspection ID: StandardJS", + "markdown": "报告 [JavaScript Standard Style](https://standardjs.com/) linter 检测到的差异。 \n\n编辑器中高亮显示的严重性基于 linter 报告的严重性级别。\n\nInspection ID: StandardJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "StandardJS", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/代码质量工具", + "index": 62, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ParametersPerFunctionJS", + "shortDescription": { + "text": "函数的形参过多" + }, + "fullDescription": { + "text": "报告形参过多的函数。 此类函数通常表明设计有问题。 使用下面的字段可指定函数的最大可接受形参数量。 Inspection ID: ParametersPerFunctionJS", + "markdown": "报告形参过多的函数。 此类函数通常表明设计有问题。\n\n\n使用下面的字段可指定函数的最大可接受形参数量。\n\nInspection ID: ParametersPerFunctionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "OverlyComplexFunctionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/函数指标", + "index": 45, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ThisExpressionReferencesGlobalObjectJS", + "shortDescription": { + "text": "引用全局对象的 'this' 表达式" + }, + "fullDescription": { + "text": "报告对象字面量或构造函数体之外的 'this' 表达式。 此类 'this' 表达式引用顶级“全局”JavaScript 对象,但大多是无用的。 Inspection ID: ThisExpressionReferencesGlobalObjectJS", + "markdown": "报告对象字面量或构造函数体之外的 ` this` 表达式。 此类 `this` 表达式引用顶级\"全局\"JavaScript 对象,但大多是无用的。\n\nInspection ID: ThisExpressionReferencesGlobalObjectJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ThisExpressionReferencesGlobalObjectJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/有效性问题", + "index": 36, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "NestedAssignmentJS", + "shortDescription": { + "text": "嵌套赋值" + }, + "fullDescription": { + "text": "报告嵌套在另一个表达式中的赋值表达式,例如 'a=b=1'。 此类表达式可能令人困惑,并违反一般设计原则,即给定结构只应发挥一种作用。 Inspection ID: NestedAssignmentJS", + "markdown": "报告嵌套在另一个表达式中的赋值表达式,例如 `a=b=1`。 此类表达式可能令人困惑,并违反一般设计原则,即给定结构只应发挥一种作用。\n\nInspection ID: NestedAssignmentJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "NestedAssignmentJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/赋值问题", + "index": 48, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "DefaultNotLastCaseInSwitchJS", + "shortDescription": { + "text": "'default' 不是 'switch' 中的最后一个 case" + }, + "fullDescription": { + "text": "报告其中的 'default' case 位于另一个 case 之前, 而不是作为最后一个 case,因而可能造成混淆的 'switch' 语句。 Inspection ID: DefaultNotLastCaseInSwitchJS", + "markdown": "报告其中的 `default` case 位于另一个 case 之前, 而不是作为最后一个 case,因而可能造成混淆的 `switch` 语句。\n\nInspection ID: DefaultNotLastCaseInSwitchJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "DefaultNotLastCaseInSwitchJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/switch 语句问题", + "index": 59, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ConfusingPlusesOrMinusesJS", + "shortDescription": { + "text": "令人困惑的 '+' 或 '-' 序列" + }, + "fullDescription": { + "text": "报告 JavaScript 代码中 '+' 或 '-' 字符的可疑组合 (例如 'a+++b') 。 此类序列令人困惑,并且它们的语义可能会因为空格的变化而变化。 Inspection ID: ConfusingPlusesOrMinusesJS", + "markdown": "报告 JavaScript 代码中 `+` 或 `-` 字符的可疑组合 (例如 `a+++b`) 。 此类序列令人困惑,并且它们的语义可能会因为空格的变化而变化。\n\nInspection ID: ConfusingPlusesOrMinusesJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ConfusingPlusesOrMinusesJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能引起混淆的代码结构", + "index": 39, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSDeprecatedSymbols", + "shortDescription": { + "text": "已使用弃用的符号" + }, + "fullDescription": { + "text": "报告弃用的函数变量的用法。 Inspection ID: JSDeprecatedSymbols", + "markdown": "报告弃用的函数变量的用法。\n\nInspection ID: JSDeprecatedSymbols" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSDeprecatedSymbols", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "LocalVariableNamingConventionJS", + "shortDescription": { + "text": "局部变量命名约定" + }, + "fullDescription": { + "text": "报告名称太短、太长或不符合 指定正则表达式模式的局部变量。 使用下面提供的字段来指定局部变量名称的最小长度、 最大长度以及预期的正则表达式。 使用标准 'java.util.regex' 格式的正则表达式。 Inspection ID: LocalVariableNamingConventionJS", + "markdown": "报告名称太短、太长或不符合 指定正则表达式模式的局部变量。\n\n\n使用下面提供的字段来指定局部变量名称的最小长度、\n最大长度以及预期的正则表达式。 使用标准 `java.util.regex` 格式的正则表达式。\n\nInspection ID: LocalVariableNamingConventionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "LocalVariableNamingConventionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/命名约定", + "index": 58, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "EmptyFinallyBlockJS", + "shortDescription": { + "text": "空 'finally' 块" + }, + "fullDescription": { + "text": "报告空的 'finally' 块,此类块通常表明有错误。 Inspection ID: EmptyFinallyBlockJS", + "markdown": "报告空的 `finally` 块,此类块通常表明有错误。\n\nInspection ID: EmptyFinallyBlockJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "EmptyFinallyBlockJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Try 语句问题", + "index": 43, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSCommentMatchesSignature", + "shortDescription": { + "text": "不匹配的 JSDoc 和函数签名" + }, + "fullDescription": { + "text": "报告 JSDoc 注释中形参的名称和数量与函数的实际形参之间不匹配的情况。 建议更新 JSDoc 注释中的形参。 示例: '/**\n * @param height Height in pixels\n */\nfunction sq(height, width) {} // 宽度未记录' 在应用快速修复后: '/**\n * @param height Height in pixels\n * @param width\n */\nfunction sq(height, width) {}' Inspection ID: JSCommentMatchesSignature", + "markdown": "报告 JSDoc 注释中形参的名称和数量与函数的实际形参之间不匹配的情况。 建议更新 JSDoc 注释中的形参。\n\n**示例:**\n\n\n /**\n * @param height Height in pixels\n */\n function sq(height, width) {} // 宽度未记录\n\n在应用快速修复后:\n\n\n /**\n * @param height Height in pixels\n * @param width\n */\n function sq(height, width) {}\n\nInspection ID: JSCommentMatchesSignature" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSCommentMatchesSignature", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "UpdateDependencyToLatestVersion", + "shortDescription": { + "text": "将 package.json 依赖项更新为最新版本" + }, + "fullDescription": { + "text": "建议将 package.json 依赖项升级到最新版本,并忽略指定的版本。 Inspection ID: UpdateDependencyToLatestVersion", + "markdown": "建议将 package.json 依赖项升级到最新版本,并忽略指定的版本。\n\nInspection ID: UpdateDependencyToLatestVersion" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "UpdateDependencyToLatestVersion", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Import 和依赖项", + "index": 55, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TypeScriptConfig", + "shortDescription": { + "text": "不一致的 Tsconfig.json 属性" + }, + "fullDescription": { + "text": "报告 tsconfig.json 文件中的 'paths' 、'checkJs' 或 'extends' 属性不一致的情况。 'checkJs' 属性必须具备 'allowJs'。 'extends' 属性应为有效的文件引用。 Inspection ID: TypeScriptConfig", + "markdown": "报告 tsconfig.json 文件中的 `paths` 、`checkJs` 或 `extends` 属性不一致的情况。 \n`checkJs` 属性必须具备 `allowJs`。 \n`extends` 属性应为有效的文件引用。\n\nInspection ID: TypeScriptConfig" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "TypeScriptConfig", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/TypeScript", + "index": 35, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSSuspiciousNameCombination", + "shortDescription": { + "text": "可疑的变量/形参名称组合" + }, + "fullDescription": { + "text": "报告目标变量或函数形参的名称与分配给它的值的名称不匹配的赋值或函数调用。 示例: 'var x = 0;\n var y = x;' 或 'var x = 0, y = 0;\n var rc = new Rectangle(y, x, 20, 20);' 此处,检查猜测 'x' 和 'y' 混淆了。 指定不应一起使用的名称。 如果形参名称或赋值目标名称包含一个组中的词语,而赋值或传递的变量的名称包含另一个组中的词语,则会报告错误。 Inspection ID: JSSuspiciousNameCombination", + "markdown": "报告目标变量或函数形参的名称与分配给它的值的名称不匹配的赋值或函数调用。\n\n示例:\n\n\n var x = 0;\n var y = x;\n\n或\n\n\n var x = 0, y = 0;\n var rc = new Rectangle(y, x, 20, 20);\n\n此处,检查猜测 `x` 和 `y` 混淆了。\n\n指定不应一起使用的名称。 如果形参名称或赋值目标名称包含一个组中的词语,而赋值或传递的变量的名称包含另一个组中的词语,则会报告错误。\n\nInspection ID: JSSuspiciousNameCombination" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSSuspiciousNameCombination", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能的 bug", + "index": 26, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSUnresolvedExtXType", + "shortDescription": { + "text": "未解析的 Ext JS xtype" + }, + "fullDescription": { + "text": "报告没有相应类的 Ext JS 'xtype' 引用。 Inspection ID: JSUnresolvedExtXType", + "markdown": "报告没有相应类的 Ext JS `xtype` 引用。\n\nInspection ID: JSUnresolvedExtXType" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JSUnresolvedExtXType", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ForLoopThatDoesntUseLoopVariableJS", + "shortDescription": { + "text": "'for' 循环中的 update 或 condition 未使用循环变量" + }, + "fullDescription": { + "text": "报告其中的条件或更新不使用 'for' 循环变量的 'for' 循环。 Inspection ID: ForLoopThatDoesntUseLoopVariableJS", + "markdown": "报告其中的条件或更新不使用 `for` 循环变量的 `for` 循环。\n\nInspection ID: ForLoopThatDoesntUseLoopVariableJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ForLoopThatDoesntUseLoopVariableJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能的 bug", + "index": 26, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TypeScriptAbstractClassConstructorCanBeMadeProtected", + "shortDescription": { + "text": "抽象类构造函数可以设为 protected" + }, + "fullDescription": { + "text": "报告 abstract 类的 public 构造函数,并建议将其设置为 protected(因为将其设置为 public 是无用的)。 Inspection ID: TypeScriptAbstractClassConstructorCanBeMadeProtected", + "markdown": "报告 abstract 类的 public 构造函数,并建议将其设置为 protected(因为将其设置为 public 是无用的)。\n\nInspection ID: TypeScriptAbstractClassConstructorCanBeMadeProtected" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "TypeScriptAbstractClassConstructorCanBeMadeProtected", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/TypeScript", + "index": 35, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "FunctionWithMultipleReturnPointsJS", + "shortDescription": { + "text": "函数具有多个返回点" + }, + "fullDescription": { + "text": "报告具有多个返回点的函数。 此类函数难以理解和维护。 Inspection ID: FunctionWithMultipleReturnPointsJS", + "markdown": "报告具有多个返回点的函数。 此类函数难以理解和维护。\n\nInspection ID: FunctionWithMultipleReturnPointsJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "FunctionWithMultipleReturnPointsJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/函数指标", + "index": 45, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSIgnoredPromiseFromCall", + "shortDescription": { + "text": "返回 promise 的方法调用结果被忽略" + }, + "fullDescription": { + "text": "报告返回日后不使用的 'Promise' 的函数调用。 此类调用通常是无意为之,表明有错误。 Inspection ID: JSIgnoredPromiseFromCall", + "markdown": "报告返回日后不使用的 `Promise` 的函数调用。 此类调用通常是无意为之,表明有错误。\n\nInspection ID: JSIgnoredPromiseFromCall" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSIgnoredPromiseFromCall", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/Async 代码和 promise", + "index": 61, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ThreeNegationsPerFunctionJS", + "shortDescription": { + "text": "函数包含三个以上的否定" + }, + "fullDescription": { + "text": "报告具有三项或更多项否定运算('!' 或 '!=')的函数。 此类函数可能造成不必要的混淆。 Inspection ID: ThreeNegationsPerFunctionJS", + "markdown": "报告具有三项或更多项否定运算(`!` 或 `!=`)的函数。 此类函数可能造成不必要的混淆。\n\nInspection ID: ThreeNegationsPerFunctionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "FunctionWithMoreThanThreeNegationsJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/函数指标", + "index": 45, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JSRemoveUnnecessaryParentheses", + "shortDescription": { + "text": "不必要的圆括号" + }, + "fullDescription": { + "text": "报告冗余圆括号。 在表达式中: 'var x = ((1) + 2) + 3' 在箭头函数实参列表中: 'var incrementer = (x) => x + 1' 在 TypeScript 和 Flow 类型声明中: 'type Card = (Suit & Rank) | (Suit & Number)' Inspection ID: JSRemoveUnnecessaryParentheses", + "markdown": "报告冗余圆括号。\n\n在表达式中:\n\n var x = ((1) + 2) + 3\n\n在箭头函数实参列表中:\n\n var incrementer = (x) => x + 1\n\n在 TypeScript 和 Flow 类型声明中:\n\n type Card = (Suit & Rank) | (Suit & Number)\n\nInspection ID: JSRemoveUnnecessaryParentheses" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JSRemoveUnnecessaryParentheses", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/代码样式问题", + "index": 19, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "OverlyComplexBooleanExpressionJS", + "shortDescription": { + "text": "过度复杂的布尔表达式" + }, + "fullDescription": { + "text": "报告包含太多项的布尔表达式。 此类表达方式可能令人困惑,并且容易出错。 使用下面的字段可指定算术表达式中允许的最大项数。 Inspection ID: OverlyComplexBooleanExpressionJS", + "markdown": "报告包含太多项的布尔表达式。 此类表达方式可能令人困惑,并且容易出错。\n\n\n使用下面的字段可指定算术表达式中允许的最大项数。\n\nInspection ID: OverlyComplexBooleanExpressionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "OverlyComplexBooleanExpressionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能引起混淆的代码结构", + "index": 39, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "OverlyComplexArithmeticExpressionJS", + "shortDescription": { + "text": "过于复杂的算术表达式" + }, + "fullDescription": { + "text": "报告包含太多项的算术表达式。 此类表达方式可能令人困惑,并且容易出错。 使用下面的字段可指定算术表达式中允许的最大项数。 Inspection ID: OverlyComplexArithmeticExpressionJS", + "markdown": "报告包含太多项的算术表达式。 此类表达方式可能令人困惑,并且容易出错。\n\n\n使用下面的字段可指定算术表达式中允许的最大项数。\n\nInspection ID: OverlyComplexArithmeticExpressionJS" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "OverlyComplexArithmeticExpressionJS", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/可能引起混淆的代码结构", + "index": 39, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ES6RedundantNestingInTemplateLiteral", + "shortDescription": { + "text": "模板字面量中的冗余嵌套" + }, + "fullDescription": { + "text": "报告字符串或模板字面量的嵌套实例。 建议将嵌套的实例内联到包含其的模板字符串中。 示例: 'let a = `Hello, ${`Brave ${\"New\"}`} ${\"World\"}!`' 应用快速修复后,代码如下所示: 'let a = `Hello, Brave New World!`' Inspection ID: ES6RedundantNestingInTemplateLiteral", + "markdown": "报告字符串或模板字面量的嵌套实例。 建议将嵌套的实例内联到包含其的模板字符串中。\n\n示例:\n\n\n let a = `Hello, ${`Brave ${\"New\"}`} ${\"World\"}!`\n\n应用快速修复后,代码如下所示:\n\n\n let a = `Hello, Brave New World!`\n\nInspection ID: ES6RedundantNestingInTemplateLiteral" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "ES6RedundantNestingInTemplateLiteral", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "StringLiteralBreaksHTMLJS", + "shortDescription": { + "text": "中断 HTML 解析的字符串字面量" + }, + "fullDescription": { + "text": "报告包含 '`" + }, + "fullDescription": { + "text": "报告后面没有跟随 `as ` 的 `extern crate self`。 Inspection ID: RsExternCrateSelfWithoutAsName", + "markdown": "报告后面没有跟随 \\`as \\\\` 的 \\`extern crate self\\`。\n\nInspection ID: RsExternCrateSelfWithoutAsName" + }, + "defaultConfiguration": { + "enabled": true, + "level": "error", + "parameters": { + "suppressToolId": "RsExternCrateSelfWithoutAsName", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Rust", + "index": 3, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RsInternalPartlyUnknownType", + "shortDescription": { + "text": "部分未知类型(内部)" + }, + "fullDescription": { + "text": "This inspection is for internal use only. Inspection ID: RsInternalPartlyUnknownType", + "markdown": "This inspection is for internal use only.\n\nInspection ID: RsInternalPartlyUnknownType" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "RsInternalPartlyUnknownType", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Rust", + "index": 3, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RsNeedlessLifetimes", + "shortDescription": { + "text": "不必要的生存期注解" + }, + "fullDescription": { + "text": "检查可以通过依赖生存期省略来移除的生存期注解。 对应于 Rust Clippy 中的 needless_lifetimes lint。 Inspection ID: RsNeedlessLifetimes", + "markdown": "检查可以通过依赖生存期省略来移除的生存期注解。 对应于 Rust Clippy 中的 [needless_lifetimes](https://rust-lang.github.io/rust-clippy/stable/#needless_lifetimes) lint。\n\nInspection ID: RsNeedlessLifetimes" + }, + "defaultConfiguration": { + "enabled": true, + "level": "note", + "parameters": { + "suppressToolId": "RsNeedlessLifetimes", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Rust/Lint", + "index": 4, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RsWrongAssocTypeArguments", + "shortDescription": { + "text": "错误的关联类型实参" + }, + "fullDescription": { + "text": "检查类型或特征引用是否使用了正确的关联类型实参。 对应于 E0191 和 E0220 Rust 错误。 Inspection ID: RsWrongAssocTypeArguments", + "markdown": "检查类型或特征引用是否使用了正确的关联类型实参。 \n对应于 [E0191](https://doc.rust-lang.org/error-index.html#E0191) 和 [E0220](https://doc.rust-lang.org/error-index.html#E0220) Rust 错误。\n\nInspection ID: RsWrongAssocTypeArguments" + }, + "defaultConfiguration": { + "enabled": true, + "level": "error", + "parameters": { + "suppressToolId": "RsWrongAssocTypeArguments", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Rust", + "index": 3, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "Docker", + "version": "261.26222.73", + "rules": [ + { + "id": "DockerFileCopyHeredoc", + "shortDescription": { + "text": "heredoc 作为 ADD/COPY 的最后一个实参(目标)" + }, + "fullDescription": { + "text": "报告在 ADD/COPY 命令中用作最后一个实参(目标)的 heredoc。 示例: 此代码定义了无效的目标: 'COPY foo < file1 && < file2 && < file3\n mkdir -p foo/bar\n FILE1' Inspection ID: DockerFileHeredocDelimiters", + "markdown": "报告文件末尾未闭合的 heredoc 分隔符。\n\n**示例:**\n此代码将报告缺失的 FILE2 和 FILE3 分隔符。\n\n\n RUN < file1 && < file2 && < file3\n mkdir -p foo/bar\n FILE1\n\nInspection ID: DockerFileHeredocDelimiters" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "DockerFileHeredocDelimiters", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Dockerfile", + "index": 5, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ComposeUnquotedPorts", + "shortDescription": { + "text": "未加引号的端口映射" + }, + "fullDescription": { + "text": "报告 Docker Compose 文件中未加引号的端口映射。 根据 Compose 文件规范,'HOST:CONTAINER' 格式的映射端口在使用小于 60 的容器端口时可能会导致错误的结果,因为 YAML 会将格式为 'xx:yy' 的数字解析为六十进制值。 因此,我们建议始终将端口映射明确指定为字符串。 示例: 'ports:\n - 3000\n - 3000-3005\n - 22:22\n - 8080:8080' 在应用快速修复后: 'ports:\n - \"3000\"\n - \"3000-3005\"\n - \"22:22\"\n - \"8080:8080\"' Inspection ID: ComposeUnquotedPorts", + "markdown": "报告 Docker Compose 文件中未加引号的端口映射。\n\n\n根据 [Compose 文件规范](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-1),`HOST:CONTAINER` 格式的映射端口在使用小于 60 的容器端口时可能会导致错误的结果,因为 YAML 会将格式为 `xx:yy` 的数字解析为六十进制值。\n因此,我们建议始终将端口映射明确指定为字符串。\n\n**示例:**\n\n\n ports:\n - 3000\n - 3000-3005\n - 22:22\n - 8080:8080\n\n在应用快速修复后:\n\n\n ports:\n - \"3000\"\n - \"3000-3005\"\n - \"22:22\"\n - \"8080:8080\"\n\nInspection ID: ComposeUnquotedPorts" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "ComposeUnquotedPorts", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Docker-compose", + "index": 38, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "DockerFileArgumentCount", + "shortDescription": { + "text": "实参数量错误" + }, + "fullDescription": { + "text": "报告 Dockerfile 命令的无效实参数量。 到达带有无效实参数量的指令后,Docker 构建将失败。 Inspection ID: DockerFileArgumentCount", + "markdown": "报告 Dockerfile 命令的无效实参数量。\n\n\n到达带有无效实参数量的指令后,Docker 构建将失败。\n\nInspection ID: DockerFileArgumentCount" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "DockerFileArgumentCount", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Dockerfile", + "index": 5, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "AngularJS", + "version": "261.26222.73", + "rules": [ + { + "id": "AngularNgOptimizedImage", + "shortDescription": { + "text": "在 img 标记中使用 ngSrc 的问题" + }, + "fullDescription": { + "text": "Reports issues related to usage of 'ngSrc' (NgOptimizedDirective) on 'img' tags. Following issues are reported: 'img' tags, which use 'src' instead of 'ngSrc' lack of 'width' and 'height', or 'fill' attributes when 'ngSrc' is used 'width' or 'height', and 'fill' attributes being present on the same element when 'ngSrc' is used Inspection ID: AngularNgOptimizedImage", + "markdown": "Reports issues related to usage of `ngSrc` ([NgOptimizedDirective](https://angular.io/guide/image-directive)) on `img` tags.\n\n\nFollowing issues are reported:\n\n* `img` tags, which use `src` instead of `ngSrc`\n* lack of `width` and `height`, or `fill` attributes when `ngSrc` is used\n* `width` or `height`, and `fill` attributes being present on the same element when `ngSrc` is used\n\nInspection ID: AngularNgOptimizedImage" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "AngularNgOptimizedImage", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Performance" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularIncorrectBlockUsage", + "shortDescription": { + "text": "Angular 块的错误用法" + }, + "fullDescription": { + "text": "Reports problems with Angular blocks. Inspection ID: AngularIncorrectBlockUsage", + "markdown": "Reports problems with Angular blocks.\n\nInspection ID: AngularIncorrectBlockUsage" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularIncorrectBlockUsage", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularInvalidTemplateReferenceVariable", + "shortDescription": { + "text": "模板引用变量未绑定或不明确" + }, + "fullDescription": { + "text": "Reports a template reference variable that is not assigned to a directive when using 'exportAs' or is assigned to multiple directives. Inspection ID: AngularInvalidTemplateReferenceVariable", + "markdown": "Reports a template reference variable that is not assigned to a directive when using `exportAs` or is assigned to multiple directives.\n\nInspection ID: AngularInvalidTemplateReferenceVariable" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularInvalidTemplateReferenceVariable", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularMisconfiguredDeferTrigger", + "shortDescription": { + "text": "@defer 触发器配置错误" + }, + "fullDescription": { + "text": "Reports misconfigured '@defer' block triggers that may lead to unexpected behavior. The inspection detects the following issues: The 'immediate' trigger makes additional triggers redundant Prefetch triggers have no effect when 'immediate' is used Prefetch timer delay that is greater than or equal to the main timer delay Prefetch triggers that match the main trigger and provide no benefit Example: '@defer (on immediate; on hover) {\n \n}\n\n@defer (on timer(1000); prefetch on timer(2000)) {\n \n}\n\n@defer (on interaction(btn); prefetch on interaction(btn)) {\n \n}' Inspection ID: AngularMisconfiguredDeferTrigger", + "markdown": "Reports misconfigured `@defer` block triggers that may lead to unexpected behavior.\n\n\nThe inspection detects the following issues:\n\n* The `immediate` trigger makes additional triggers redundant\n* Prefetch triggers have no effect when `immediate` is used\n* Prefetch timer delay that is greater than or equal to the main timer delay\n* Prefetch triggers that match the main trigger and provide no benefit\n\n\nExample:\n\n\n @defer (on immediate; on hover) {\n \n }\n\n @defer (on timer(1000); prefetch on timer(2000)) {\n \n }\n\n @defer (on interaction(btn); prefetch on interaction(btn)) {\n \n }\n\nInspection ID: AngularMisconfiguredDeferTrigger" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "AngularMisconfiguredDeferTrigger", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularConstantReassignment", + "shortDescription": { + "text": "尝试分配给常量或只读变量" + }, + "fullDescription": { + "text": "Reports reassigning a value to a constant or a readonly variable. Inspection ID: AngularConstantReassignment", + "markdown": "Reports reassigning a value to a constant or a readonly variable.\n\nInspection ID: AngularConstantReassignment" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "AngularConstantReassignment", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularMultipleStructuralDirectives", + "shortDescription": { + "text": "一个元素上有多个结构指令" + }, + "fullDescription": { + "text": "Reports multiple structural directives ('*ngIf', '*ngFor', etc.) on one element. Inspection ID: AngularMultipleStructuralDirectives", + "markdown": "Reports multiple structural directives (`*ngIf`, `*ngFor`, etc.) on one element.\n\nInspection ID: AngularMultipleStructuralDirectives" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularMultipleStructuralDirectives", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularUnresolvedPipe", + "shortDescription": { + "text": "未解析的管道" + }, + "fullDescription": { + "text": "Reports an unresolved pipe. Inspection ID: AngularUnresolvedPipe", + "markdown": "Reports an unresolved pipe.\n\nInspection ID: AngularUnresolvedPipe" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularUnresolvedPipe", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularCliAddDependency", + "shortDescription": { + "text": "Angular CLI 添加依赖项" + }, + "fullDescription": { + "text": "Suggests using the 'ng add' command to install the dependency. 'ng add' will use the package manager to download it and invoke a schematic which can update your project with configuration changes, add additional dependencies (e.g. polyfills), or scaffold package-specific initialization code. Inspection ID: AngularCliAddDependency", + "markdown": "Suggests using the `ng add` command to install the dependency.\n\n`ng add` will use the package manager to download it and invoke a schematic\nwhich can update your project with configuration changes, add additional dependencies (e.g. polyfills),\nor scaffold package-specific initialization code.\n\nInspection ID: AngularCliAddDependency" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "AngularCliAddDependency", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularDeferBlockOnTrigger", + "shortDescription": { + "text": "@defer `on` 触发器的问题" + }, + "fullDescription": { + "text": "Reports issues with triggers in `on` parameters in `@defer` block. Inspection ID: AngularDeferBlockOnTrigger", + "markdown": "Reports issues with triggers in \\`on\\` parameters in \\`@defer\\` block.\n\nInspection ID: AngularDeferBlockOnTrigger" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularDeferBlockOnTrigger", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularInaccessibleSymbol", + "shortDescription": { + "text": "无法访问的组件成员或指令输入" + }, + "fullDescription": { + "text": "Reports access to invisible (private or protected) component member or directive input from an Angular template. Inspection ID: AngularInaccessibleSymbol", + "markdown": "Reports access to invisible (private or protected) component member or directive input from an Angular template.\n\nInspection ID: AngularInaccessibleSymbol" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularInaccessibleSymbol", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularMissingEventHandler", + "shortDescription": { + "text": "缺少事件处理程序" + }, + "fullDescription": { + "text": "Reports a missing event handler statement for an event binding. Inspection ID: AngularMissingEventHandler", + "markdown": "Reports a missing event handler statement for an event binding.\n\nInspection ID: AngularMissingEventHandler" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularMissingEventHandler", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularUnusedComponentImport", + "shortDescription": { + "text": "Angular 组件声明中未使用的 import" + }, + "fullDescription": { + "text": "Reports unused imports in Angular components. Inspection ID: AngularUnusedComponentImport", + "markdown": "Reports unused imports in Angular components.\n\nInspection ID: AngularUnusedComponentImport" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularUnusedComponentImport", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularUndefinedTag", + "shortDescription": { + "text": "未定义的标记" + }, + "fullDescription": { + "text": "Reports a tag defined by a component or directive out of the current scope. Inspection ID: AngularUndefinedTag", + "markdown": "Reports a tag defined by a component or directive out of the current scope.\n\nInspection ID: AngularUndefinedTag" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularUndefinedTag", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularUndefinedBinding", + "shortDescription": { + "text": "未定义的绑定" + }, + "fullDescription": { + "text": "Reports an undefined property, event, or structural directive bindings on elements. Inspection ID: AngularUndefinedBinding", + "markdown": "Reports an undefined property, event, or structural directive bindings on elements.\n\nInspection ID: AngularUndefinedBinding" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularUndefinedBinding", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularInvalidImportedOrDeclaredSymbol", + "shortDescription": { + "text": "导入或声明的符号无效" + }, + "fullDescription": { + "text": "Reports any symbol that is declared, imported or exported by an Angular module or standalone component that is not a module, component, directive, or pipe or can’t be used in the context of the property. Inspection ID: AngularInvalidImportedOrDeclaredSymbol", + "markdown": "Reports any symbol that is declared, imported or exported by an Angular module or standalone component that is not a module, component, directive, or pipe or can't be used in the context of the property.\n\nInspection ID: AngularInvalidImportedOrDeclaredSymbol" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularInvalidImportedOrDeclaredSymbol", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularIllegalForLoopTrackAccess", + "shortDescription": { + "text": "非法 @for 循环访问" + }, + "fullDescription": { + "text": "Reports illegal access to the template variable within '@for' loop 'track' expression. Inspection ID: AngularIllegalForLoopTrackAccess", + "markdown": "Reports illegal access to the template variable within `@for` loop `track` expression.\n\nInspection ID: AngularIllegalForLoopTrackAccess" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularIllegalForLoopTrackAccess", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularAmbiguousComponentTag", + "shortDescription": { + "text": "不明确的组件标记" + }, + "fullDescription": { + "text": "Reports a component that is matched on an embedded template element '' or multiple components matched on any other element. Inspection ID: AngularAmbiguousComponentTag", + "markdown": "Reports a component that is matched on an embedded template element `` or multiple components matched on any other element.\n\nInspection ID: AngularAmbiguousComponentTag" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularAmbiguousComponentTag", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularUndefinedModuleExport", + "shortDescription": { + "text": "未定义从 Angular 模块导出" + }, + "fullDescription": { + "text": "Reports an export of an undeclared or unimported component, directive, or pipes from an Angular module. Inspection ID: AngularUndefinedModuleExport", + "markdown": "Reports an export of an undeclared or unimported component, directive, or pipes from an Angular module.\n\nInspection ID: AngularUndefinedModuleExport" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularUndefinedModuleExport", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularInvalidI18nAttribute", + "shortDescription": { + "text": "i18n 特性无效" + }, + "fullDescription": { + "text": "Reports a problem with a 'i18n-*' attribute. Inspection ID: AngularInvalidI18nAttribute", + "markdown": "Reports a problem with a `i18n-*` attribute.\n\nInspection ID: AngularInvalidI18nAttribute" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "AngularInvalidI18nAttribute", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularForBlockNonIterableVar", + "shortDescription": { + "text": "@for 块中的不可迭代类型" + }, + "fullDescription": { + "text": "Reports that the type of variable to iterate over does not have '[Symbol.iterator]()' method, which returns an iterator. Inspection ID: AngularForBlockNonIterableVar", + "markdown": "Reports that the type of variable to iterate over does not have `[Symbol.iterator]()` method, which returns an iterator.\n\nInspection ID: AngularForBlockNonIterableVar" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularForBlockNonIterableVar", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularUncalledSignalLengthPropertyAccess", + "shortDescription": { + "text": "正在访问未调用信号的 length 属性" + }, + "fullDescription": { + "text": "Reports access to the length property of an uncalled Angular signal, which is always 0, since signal calls do not accept any arguments. Such access is most likely a mistake and the signal should be first called instead. Inspection ID: AngularUncalledSignalLengthPropertyAccess", + "markdown": "Reports access to the length property of an uncalled Angular signal, which is always 0, since signal calls do not accept any arguments. Such access is most likely a mistake and the signal should be first called instead.\n\nInspection ID: AngularUncalledSignalLengthPropertyAccess" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "AngularUncalledSignalLengthPropertyAccess", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularIncorrectTemplateDefinition", + "shortDescription": { + "text": "组件模板定义不正确" + }, + "fullDescription": { + "text": "Reports a component that doesn’t have an associated template or uses both 'template' and 'templateUrl' properties. Inspection ID: AngularIncorrectTemplateDefinition", + "markdown": "Reports a component that doesn't have an associated template or uses both `template` and `templateUrl` properties.\n\nInspection ID: AngularIncorrectTemplateDefinition" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularIncorrectTemplateDefinition", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularIncorrectLetUsage", + "shortDescription": { + "text": "@let 声明的错误用法" + }, + "fullDescription": { + "text": "Reports problems with @let declaration usages. Inspection ID: AngularIncorrectLetUsage", + "markdown": "Reports problems with @let declaration usages.\n\nInspection ID: AngularIncorrectLetUsage" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularIncorrectLetUsage", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularNonStandaloneComponentImports", + "shortDescription": { + "text": "非独立组件中的无效 import 用法" + }, + "fullDescription": { + "text": "Reports usages of imports property in non-standalone component decorators. Imports can be used only in standalone components. Inspection ID: AngularNonStandaloneComponentImports", + "markdown": "Reports usages of imports property in non-standalone component decorators. Imports can be used only in standalone components.\n\nInspection ID: AngularNonStandaloneComponentImports" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularNonStandaloneComponentImports", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularMissingOrInvalidDeclarationInModule", + "shortDescription": { + "text": "模块中的组件、指令或管道声明缺失或无效" + }, + "fullDescription": { + "text": "Reports a non-standalone Angular component, directive, or pipe that is not declared in any module or is declared in multiple modules. Inspection ID: AngularMissingOrInvalidDeclarationInModule", + "markdown": "Reports a non-standalone Angular component, directive, or pipe that is not declared in any module or is declared in multiple modules.\n\nInspection ID: AngularMissingOrInvalidDeclarationInModule" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularMissingOrInvalidDeclarationInModule", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularInvalidAnimationTriggerAssignment", + "shortDescription": { + "text": "动画触发器赋值无效" + }, + "fullDescription": { + "text": "Reports an invalid assignment of an animation trigger. To attach an animation to an element, use '[@triggerName]=\"expression\"' or an attribute without a value '@triggerName'. Inspection ID: AngularInvalidAnimationTriggerAssignment", + "markdown": "Reports an invalid assignment of an animation trigger. To attach an animation to an element, use `[@triggerName]=\"expression\"` or an attribute without a value `@triggerName`.\n\nInspection ID: AngularInvalidAnimationTriggerAssignment" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularInvalidAnimationTriggerAssignment", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularInvalidEntryComponent", + "shortDescription": { + "text": "入口组件无效" + }, + "fullDescription": { + "text": "Reports an invalid Angular component specified in the module’s 'bootstrap' or 'entryComponents' property. Inspection ID: AngularInvalidEntryComponent", + "markdown": "Reports an invalid Angular component specified in the module's `bootstrap` or `entryComponents` property.\n\nInspection ID: AngularInvalidEntryComponent" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularInvalidEntryComponent", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularRecursiveModuleImportExport", + "shortDescription": { + "text": "递归导入或导出 Angular 模块或独立组件" + }, + "fullDescription": { + "text": "Reports a cyclic dependency between Angular modules or standalone components. Inspection ID: AngularRecursiveModuleImportExport", + "markdown": "Reports a cyclic dependency between Angular modules or standalone components.\n\nInspection ID: AngularRecursiveModuleImportExport" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularRecursiveModuleImportExport", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularInvalidSelector", + "shortDescription": { + "text": "选择器缺失或无效" + }, + "fullDescription": { + "text": "Reports an invalid 'selector' property of a component or directive. Inspection ID: AngularInvalidSelector", + "markdown": "Reports an invalid `selector` property of a component or directive.\n\nInspection ID: AngularInvalidSelector" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularInvalidSelector", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularBindingTypeMismatch", + "shortDescription": { + "text": "无效的绑定类型" + }, + "fullDescription": { + "text": "Reports a mismatch between actual and expected directive binding type. Inspection ID: AngularBindingTypeMismatch", + "markdown": "Reports a mismatch between actual and expected directive binding type.\n\nInspection ID: AngularBindingTypeMismatch" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularBindingTypeMismatch", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularNonEmptyNgContent", + "shortDescription": { + "text": "内容位于 标记中" + }, + "fullDescription": { + "text": "Reports a text or tag occurrence inside a '' tag used for content projection. Inspection ID: AngularNonEmptyNgContent", + "markdown": "Reports a text or tag occurrence inside a `` tag used for content projection.\n\nInspection ID: AngularNonEmptyNgContent" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularNonEmptyNgContent", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularUnsupportedSyntax", + "shortDescription": { + "text": "不支持的 Angular 表达式语法" + }, + "fullDescription": { + "text": "Reports problems with Angular expression syntax, which is not supported in an older version of Angular. Inspection ID: AngularUnsupportedSyntax", + "markdown": "Reports problems with Angular expression syntax, which is not supported in an older version of Angular.\n\nInspection ID: AngularUnsupportedSyntax" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularUnsupportedSyntax", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularMissingRequiredDirectiveInputBinding", + "shortDescription": { + "text": "缺少必需的指令输入" + }, + "fullDescription": { + "text": "Reports a missing binding for a required directive input. Inspection ID: AngularMissingRequiredDirectiveInputBinding", + "markdown": "Reports a missing binding for a required directive input.\n\nInspection ID: AngularMissingRequiredDirectiveInputBinding" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AngularMissingRequiredDirectiveInputBinding", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AngularInsecureBindingToEvent", + "shortDescription": { + "text": "绑定到事件不安全" + }, + "fullDescription": { + "text": "Reports a binding to an event property or attribute, for example, '[onclick]' or '[attr.onclick]' instead of '(click)'. Inspection ID: AngularInsecureBindingToEvent", + "markdown": "Reports a binding to an event property or attribute, for example, `[onclick]` or `[attr.onclick]` instead of `(click)`.\n\nInspection ID: AngularInsecureBindingToEvent" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "AngularInsecureBindingToEvent", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Security" + } + }, + "relationships": [ + { + "target": { + "id": "Angular", + "index": 8, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "org.intellij.plugins.postcss", + "version": "261.26222.73", + "rules": [ + { + "id": "PostCssCustomSelector", + "shortDescription": { + "text": "自定义选择器无效" + }, + "fullDescription": { + "text": "报告 PostCSS 自定义选择器中的语法错误。 示例: '@custom-selector :--heading h1, h2, h3;' Inspection ID: PostCssCustomSelector", + "markdown": "报告 [PostCSS 自定义选择器](https://github.com/postcss/postcss-custom-selectors)中的语法错误。\n\n示例:\n\n\n @custom-selector :--heading h1, h2, h3;\n\nInspection ID: PostCssCustomSelector" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "PostCssCustomSelector", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "PostCSS", + "index": 9, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "PostCssUnresolvedModuleValueReference", + "shortDescription": { + "text": "未解析的 CSS 模块值" + }, + "fullDescription": { + "text": "报告对 CSS Module Value('@value' 声明)的未解析引用。 示例: '@value foo from unknown;' Inspection ID: PostCssUnresolvedModuleValueReference", + "markdown": "报告对 [CSS Module Value](https://github.com/css-modules/postcss-modules-values)(`@value` 声明)的未解析引用。\n\n示例:\n\n\n @value foo from unknown;\n\nInspection ID: PostCssUnresolvedModuleValueReference" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "PostCssUnresolvedModuleValueReference", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "PostCSS", + "index": 9, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "PostCssNesting", + "shortDescription": { + "text": "无效的嵌套规则" + }, + "fullDescription": { + "text": "报告语法不符合 PostCSS Nested 或 PostCSS Nesting 规范的嵌套样式规则。 示例: '.phone {\n &_title {}\n}' Inspection ID: PostCssNesting", + "markdown": "报告语法不符合 [PostCSS Nested](https://github.com/postcss/postcss-nested) 或 [PostCSS Nesting](https://github.com/csstools/postcss-nesting) 规范的嵌套样式规则。\n\n示例:\n\n\n .phone {\n &_title {}\n }\n\nInspection ID: PostCssNesting" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "PostCssNesting", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "PostCSS", + "index": 9, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "PostCssCustomMedia", + "shortDescription": { + "text": "自定义媒体无效" + }, + "fullDescription": { + "text": "报告 PostCSS 自定义媒体查询中的语法错误。 示例: '@custom-media --small-viewport (max-width: 30em);' Inspection ID: PostCssCustomMedia", + "markdown": "报告 [PostCSS 自定义媒体](https://github.com/postcss/postcss-custom-media)查询中的语法错误。\n\n示例:\n\n\n @custom-media --small-viewport (max-width: 30em);\n\nInspection ID: PostCssCustomMedia" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "PostCssCustomMedia", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "PostCSS", + "index": 9, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "PostCssMediaRange", + "shortDescription": { + "text": "媒体查询范围无效" + }, + "fullDescription": { + "text": "检查范围上下文语法,该语法可替代性地用于 'range' 类型的媒体特性。 示例: '@media screen and (500px <= width <= 1200px) {}' Inspection ID: PostCssMediaRange", + "markdown": "检查[范围上下文](https://github.com/postcss/postcss-media-minmax)语法,该语法可替代性地用于 'range' 类型的媒体特性。\n\n示例:\n\n\n @media screen and (500px <= width <= 1200px) {}\n\nInspection ID: PostCssMediaRange" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "PostCssMediaRange", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "PostCSS", + "index": 9, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "dev.blachut.svelte.lang", + "version": "261.26222.22", + "rules": [ + { + "id": "SvelteEmptyGenerics", + "shortDescription": { + "text": "空的 generics 特性" + }, + "fullDescription": { + "text": "Reports empty or whitespace-only 'generics' attributes on script tags. Example of invalid code: '' Example of valid code: '' Inspection ID: SvelteEmptyGenerics", + "markdown": "Reports empty or whitespace-only `generics` attributes on script tags.\n\nExample of invalid code:\n\n \n\nExample of valid code:\n\n \n\nInspection ID: SvelteEmptyGenerics" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "SvelteEmptyGenerics", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Svelte", + "index": 10, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SvelteGenericsOnInstanceScriptOnly", + "shortDescription": { + "text": "仅允许在实例脚本中使用泛型" + }, + "fullDescription": { + "text": "Reports 'generics' attributes on module scripts ('context=\"module\"'). The 'generics' attribute is only allowed on instance scripts, not module scripts. Example of invalid code: '' Example of valid code: '' Inspection ID: SvelteGenericsOnInstanceScriptOnly", + "markdown": "Reports `generics` attributes on module scripts (`context=\"module\"`).\n\nThe `generics` attribute is only allowed on instance scripts, not module scripts.\n\nExample of invalid code:\n\n \n\nExample of valid code:\n\n \n\nInspection ID: SvelteGenericsOnInstanceScriptOnly" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "SvelteGenericsOnInstanceScriptOnly", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Svelte", + "index": 10, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SvelteGenericsRequiresTypeScript", + "shortDescription": { + "text": "Generics 特性需要 TypeScript" + }, + "fullDescription": { + "text": "Reports 'generics' attributes on script tags without TypeScript ('lang=\"ts\"'). The 'generics' attribute requires TypeScript to be enabled on the script tag. Example of invalid code: '' Example of valid code: '' Inspection ID: SvelteGenericsRequiresTypeScript", + "markdown": "Reports `generics` attributes on script tags without TypeScript (`lang=\"ts\"`).\n\nThe `generics` attribute requires TypeScript to be enabled on the script tag.\n\nExample of invalid code:\n\n \n\nExample of valid code:\n\n \n\nInspection ID: SvelteGenericsRequiresTypeScript" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "SvelteGenericsRequiresTypeScript", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Svelte", + "index": 10, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SvelteUnresolvedComponent", + "shortDescription": { + "text": "未解析的 Svelte 组件" + }, + "fullDescription": { + "text": "此检查会检查未解析的 Svelte 组件 Inspection ID: SvelteUnresolvedComponent", + "markdown": "此检查会检查未解析的 Svelte 组件\n\nInspection ID: SvelteUnresolvedComponent" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "SvelteUnresolvedComponent", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Svelte", + "index": 10, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "XPathView", + "version": "261.26222.73", + "rules": [ + { + "id": "IndexZeroUsage", + "shortDescription": { + "text": "索引为 0 的 XPath 谓词" + }, + "fullDescription": { + "text": "报告 '0' 在谓词索引中或者与函数 'position()' 的 比较中的用法。 此类用法几乎在所有情况下都是 bug,因为在 XPath 中,索引从 '1' 而不是 '0' 开始。 示例: '//someelement[position() = 0]' or '//something[0]' Inspection ID: IndexZeroUsage", + "markdown": "报告 `0` 在谓词索引中或者与函数 `position()` 的 比较中的用法。 此类用法几乎在所有情况下都是 bug,因为在 XPath 中,索引从 `1` 而*不是* `0` 开始。\n\n**示例:**\n\n\n //someelement[position() = 0] or //something[0]\n\nInspection ID: IndexZeroUsage" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "IndexZeroUsage", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "XPath", + "index": 11, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CheckNodeTest", + "shortDescription": { + "text": "未知的元素或特性名称" + }, + "fullDescription": { + "text": "报告在 XPath 表达式中使用但在关联的 XML 文件中缺少并且也没有在引用的架构中定义的元素或特性的名称。 此类名称通常是拼写错误所致,否则可能只会在运行时被发现。 示例: '' 如果 'h' 被绑定到 XHTML 命名空间,检查会将 'match' 表达式的这一部分报告为未知元素名称,因为该元素的正确名称是 \"textarea\"。 Inspection ID: CheckNodeTest", + "markdown": "报告在 XPath 表达式中使用但在关联的 XML 文件中缺少并且也没有在引用的架构中定义的元素或特性的名称。 此类名称通常是拼写错误所致,否则可能只会在运行时被发现。\n\n**示例:**\n\n\n \n\n\n如果 `h` 被绑定到 XHTML 命名空间,检查会将 `match` 表达式的这一部分报告为未知元素名称,因为该元素的正确名称是 \"textarea\"。\n\nInspection ID: CheckNodeTest" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CheckNodeTest", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "XPath", + "index": 11, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XsltUnusedDeclaration", + "shortDescription": { + "text": "未使用的变量或形参" + }, + "fullDescription": { + "text": "报告从未被使用的局部变量和形参。 Inspection ID: XsltUnusedDeclaration", + "markdown": "报告从未被使用的局部变量和形参。\n\nInspection ID: XsltUnusedDeclaration" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "XsltUnusedDeclaration", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "XSLT", + "index": 54, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XsltDeclarations", + "shortDescription": { + "text": "不正确的声明" + }, + "fullDescription": { + "text": "报告 XSLT 变量、形参和命名模板中的重复声明和非法标识符: Inspection ID: XsltDeclarations", + "markdown": "报告 XSLT 变量、形参和命名模板中的重复声明和非法标识符:\n\nInspection ID: XsltDeclarations" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "XsltDeclarations", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "XSLT", + "index": 54, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XsltVariableShadowing", + "shortDescription": { + "text": "隐藏的变量" + }, + "fullDescription": { + "text": "报告隐藏的 XSLT 变量。 Inspection ID: XsltVariableShadowing", + "markdown": "报告隐藏的 XSLT 变量。\n\nInspection ID: XsltVariableShadowing" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "XsltVariableShadowing", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "XSLT", + "index": 54, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HardwiredNamespacePrefix", + "shortDescription": { + "text": "硬编码命名空间前缀" + }, + "fullDescription": { + "text": "报告 'name()' 函数与包含冒号 (':') 的字符串的比较。 此类用法通常表明在比较中存在硬编码的命名空间前缀。 因此,针对为相同命名空间使用另一个前缀的 XML 运行时,代码将中断。 示例: '...' Inspection ID: HardwiredNamespacePrefix", + "markdown": "报告 `name()` 函数与包含冒号 (`:`) 的字符串的比较。 此类用法通常表明在比较中存在硬编码的命名空间前缀。 因此,针对为相同命名空间使用另一个前缀的 XML 运行时,代码将中断。\n\n**示例:**\n\n\n ...\n\nInspection ID: HardwiredNamespacePrefix" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "HardwiredNamespacePrefix", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "XPath", + "index": 11, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RedundantTypeConversion", + "shortDescription": { + "text": "冗余类型转换" + }, + "fullDescription": { + "text": "报告不必要的类型转换。 当 'string()'、'number()' 或 'boolean()' 函数的实参类型已经与函数的返回类型相同时,或者如果 预期表达式类型为 'any',无需类型转换。 建议移除不必要的转换。 Inspection ID: RedundantTypeConversion", + "markdown": "报告不必要的类型转换。 当 `string()`、`number()` 或 `boolean()` 函数的实参类型已经与函数的返回类型相同时,或者如果 预期表达式类型为 `any`,无需类型转换。 建议移除不必要的转换。\n\nInspection ID: RedundantTypeConversion" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "RedundantTypeConversion", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "XPath", + "index": 11, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XsltTemplateInvocation", + "shortDescription": { + "text": "不正确的模板调用" + }, + "fullDescription": { + "text": "报告缺少实参、传递未被声明的实参,以及在命名 XSLT 模板调用中多次传递形参的实参。 使用默认值声明的形参是可选形参,不会被报告为缺失。 Inspection ID: XsltTemplateInvocation", + "markdown": "报告缺少实参、传递未被声明的实参,以及在命名 XSLT 模板调用中多次传递形参的实参。\n\n\n使用默认值声明的形参是可选形参,不会被报告为缺失。\n\nInspection ID: XsltTemplateInvocation" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "XsltTemplateInvocation", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "XSLT", + "index": 54, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ImplicitTypeConversion", + "shortDescription": { + "text": "隐式类型转换" + }, + "fullDescription": { + "text": "报告在预定义的 XPath 类型 'STRING'、'NUMBER'、'BOOLEAN' 和 'NODESET' 之间进行的隐式转换。 有助于编写类型表现更明确的XSLT脚本,且避免难以捉摸的错误。 示例: '' 不同于 '' 第一个测试检查元素 “ foo” 是否存在('count(foo) > 0)',而第二个测试仅当元素包含文本时 ('string-length(foo) > 0'),才会为 “true”。 建议使 类型转换更加明确。 使用以下选项配置检查: 启用或禁用某些类型之间的隐式转换 总是报告没有导致实际预期类型的显式转换,例如, '' 忽略使用 'string()' 函数作为编写 'string-length() > 0' 的快捷方式而造成的从 'NODESET' 到 'BOOLEAN' 的转换。 Inspection ID: ImplicitTypeConversion", + "markdown": "报告在预定义的 XPath 类型 `STRING`、`NUMBER`、`BOOLEAN` 和 `NODESET` 之间进行的隐式转换。 有助于编写类型表现更明确的XSLT脚本,且避免难以捉摸的错误。\n\n**示例:**\n\n\n \n\n不同于\n\n\n \n\n\n第一个测试检查元素 \" foo\" 是否存在(`count(foo) > 0)`,而第二个测试仅当元素包含文本时 (`string-length(foo) > 0`),才会为 \"true\"。 建议使\n类型转换更加明确。\n\n\n使用以下选项配置检查:\n\n* 启用或禁用某些类型之间的隐式转换\n* 总是报告没有导致实际预期类型的显式转换,例如, ``\n* 忽略使用 `string()` 函数作为编写 `string-length() > 0` 的快捷方式而造成的从 `NODESET` 到 `BOOLEAN` 的转换。\n\nInspection ID: ImplicitTypeConversion" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ImplicitTypeConversion", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "XPath", + "index": 11, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "org.jetbrains.plugins.sass", + "version": "261.26222.73", + "rules": [ + { + "id": "SassScssUnresolvedMixin", + "shortDescription": { + "text": "未解析的 mixin" + }, + "fullDescription": { + "text": "报告未解析的 Sass/SCSS mixin 引用。 示例: '* {\n @include unknown-mixin;\n}' Inspection ID: SassScssUnresolvedMixin", + "markdown": "报告未解析的 [Sass/SCSS mixin](https://sass-lang.com/documentation/at-rules/mixin) 引用。\n\n**示例:**\n\n\n * {\n @include unknown-mixin;\n }\n\nInspection ID: SassScssUnresolvedMixin" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SassScssUnresolvedMixin", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Sass_SCSS", + "index": 12, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SassScssResolvedByNameOnly", + "shortDescription": { + "text": "缺少 import" + }, + "fullDescription": { + "text": "报告对另一个文件(但该文件未在当前文件中显式导入)中声明的变量、mixin 或函数的引用。 示例: '* {\n margin: $var-in-other-file;\n}' Inspection ID: SassScssResolvedByNameOnly", + "markdown": "报告对另一个文件(但该文件未在当前文件中显式[导入](https://sass-lang.com/documentation/at-rules/import))中声明的变量、mixin 或函数的引用。\n\n**示例:**\n\n\n * {\n margin: $var-in-other-file;\n }\n\nInspection ID: SassScssResolvedByNameOnly" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "SassScssResolvedByNameOnly", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Sass_SCSS", + "index": 12, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SassScssUnresolvedPlaceholderSelector", + "shortDescription": { + "text": "未解析的占位符选择器" + }, + "fullDescription": { + "text": "报告未解析的 Sass/SCSS 占位符选择器引用。 示例: '* {\n @extend %unknown-placeholder-selector;\n}' Inspection ID: SassScssUnresolvedPlaceholderSelector", + "markdown": "报告未解析的 [Sass/SCSS 占位符选择器](https://sass-lang.com/documentation/variables)引用。\n\n**示例:**\n\n\n * {\n @extend %unknown-placeholder-selector;\n }\n\nInspection ID: SassScssUnresolvedPlaceholderSelector" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SassScssUnresolvedPlaceholderSelector", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Sass_SCSS", + "index": 12, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SassScssUnresolvedVariable", + "shortDescription": { + "text": "未解析的变量" + }, + "fullDescription": { + "text": "报告未解析的 Sass/SCSS 变量引用。 示例: '* {\n margin: $unknown-var;\n}' Inspection ID: SassScssUnresolvedVariable", + "markdown": "报告未解析的 [Sass/SCSS 变量](https://sass-lang.com/documentation/variables)引用。\n\n**示例:**\n\n\n * {\n margin: $unknown-var;\n }\n\nInspection ID: SassScssUnresolvedVariable" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SassScssUnresolvedVariable", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Sass_SCSS", + "index": 12, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "com.jetbrains.sh", + "version": "261.26222.73", + "rules": [ + { + "id": "ShellCheck", + "shortDescription": { + "text": "ShellCheck" + }, + "fullDescription": { + "text": "报告由集成的 ShellCheck 静态分析工具检测到的 shell 脚本错误。 Inspection ID: ShellCheck", + "markdown": "报告由集成的 [ShellCheck](https://github.com/koalaman/shellcheck) 静态分析工具检测到的 shell 脚本错误。\n\nInspection ID: ShellCheck" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "ShellCheck", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Shell 脚本", + "index": 13, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "Karma", + "version": "261.26222.73", + "rules": [ + { + "id": "KarmaConfigFile", + "shortDescription": { + "text": "无效的 Karma 配置文件" + }, + "fullDescription": { + "text": "报告 Karma 配置文件 (例如 'karma.conf.js') 的文件路径 ('basePath'、'files') 中的潜在错误。 Inspection ID: KarmaConfigFile", + "markdown": "报告 Karma 配置文件 (例如 `karma.conf.js`) 的文件路径 ('basePath'、'files') 中的潜在错误。\n\nInspection ID: KarmaConfigFile" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "KarmaConfigFile", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/单元测试", + "index": 14, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "tanvd.grazi", + "version": "261.26222.73", + "rules": [ + { + "id": "GrazieStyle", + "shortDescription": { + "text": "样式" + }, + "fullDescription": { + "text": "查看在以下位置定义的编写样式: 用于此项目或其特定子目录的 Grazie 规则文件(例如,英语的 '.grazie.en.yaml')。 要创建此类文件,请在项目的任何目录(例如,根)上调用新建菜单。 编辑器 | 自然语言 | 规则设置中的样式规则 此检查仅通过代码 | 分析代码 | 按名称运行检查… 或离线分析返回结果。 编辑器对样式问题的高亮显示独立于此检查的设置进行。 Inspection ID: GrazieStyle", + "markdown": "查看在以下位置定义的编写样式:\n\n* 用于此项目或其特定子目录的 Grazie 规则文件(例如,英语的 `.grazie.en.yaml`)。 要创建此类文件,请在项目的任何目录(例如,根)上调用**新建**菜单。\n* *编辑器 \\| 自然语言 \\| 规则* 设置中的*样式*规则\n\n此检查仅通过**代码 \\| 分析代码 \\| 按名称运行检查...** 或离线分析返回结果。 编辑器对样式问题的高亮显示独立于此检查的设置进行。\n\nInspection ID: GrazieStyle" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "GrazieStyle", + "ideaSeverity": "STYLE_SUGGESTION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "校对", + "index": 15, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SpellCheckingInspection", + "shortDescription": { + "text": "拼写" + }, + "fullDescription": { + "text": "报告代码、注释和字面量中的拼写错误并一键修正。 Inspection ID: SpellCheckingInspection", + "markdown": "报告代码、注释和字面量中的拼写错误并一键修正。\n\nInspection ID: SpellCheckingInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "SpellCheckingInspection", + "ideaSeverity": "TYPO", + "qodanaSeverity": "Low", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "校对", + "index": 15, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "LanguageDetectionInspection", + "shortDescription": { + "text": "自然语言检测" + }, + "fullDescription": { + "text": "检测自然语言并建议启用相应的语法和拼写检查。 Inspection ID: LanguageDetectionInspection", + "markdown": "检测自然语言并建议启用相应的语法和拼写检查。\n\nInspection ID: LanguageDetectionInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "LanguageDetectionInspection", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "校对", + "index": 15, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "GrazieInspection", + "shortDescription": { + "text": "语法" + }, + "fullDescription": { + "text": "报告文本中的语法错误。 您可以在 设置 | 编辑器 | 自然语言 | 语法和样式中配置检查。 Inspection ID: GrazieInspection", + "markdown": "报告文本中的语法错误。 您可以在 [设置 \\| 编辑器 \\| 自然语言 \\| 语法和样式](settings://reference.settingsdialog.project.grazie)中配置检查。\n\nInspection ID: GrazieInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "GrazieInspection", + "ideaSeverity": "GRAMMAR_ERROR", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "校对", + "index": 15, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "GrazieInspectionRunner", + "shortDescription": { + "text": "语法" + }, + "fullDescription": { + "text": "在此处编写您的描述。 使用英语时,描述以第三人称单数动词开头,如 \"reports\"、\"detects\" 和 \"highlights\";使用中文时,描述以动词开头,如“报告”、“检测”和“高亮显示”。 在第一句话中,简要说明此检查能帮助您检测什么。 请确保句子不要太长或太复杂。 第一句话必须放在一个专门的段落中,与正文的其余部分分开。 这将使描述更易于阅读。 请确保描述不是对检查标题的简单重复。 有关详情,请参阅 https://plugins.jetbrains.com/docs/intellij/inspections.html#descriptions。 嵌入代码段: '// 根据检查注册 'language' 特性自动高亮显示' 此注释后的文本将仅显示在检查的设置中。 要直接从描述中打开相关设置,请添加一个带有 `settings://$` 的链接,可以选择在后面加上 `?$` 来预选 UI 元素。 Inspection ID: GrazieInspectionRunner", + "markdown": "在此处编写您的描述。 使用英语时,描述以第三人称单数动词开头,如 \"reports\"、\"detects\" 和 \"highlights\";使用中文时,描述以动词开头,如\"报告\"、\"检测\"和\"高亮显示\"。 在第一句话中,简要说明此检查能帮助您检测什么。 请确保句子不要太长或太复杂。\n\n\n第一句话必须放在一个专门的段落中,与正文的其余部分分开。 这将使描述更易于阅读。\n请确保描述不是对检查标题的简单重复。\n\n\n有关详情,请参阅 https://plugins.jetbrains.com/docs/intellij/inspections.html#descriptions。\n\n\n嵌入代码段:\n\n\n // 根据检查注册 'language' 特性自动高亮显示\n\n此注释后的文本将仅显示在检查的设置中。\n\n要直接从描述中打开相关设置,请添加一个带有 \\`settings://$\\` 的链接,可以选择在后面加上 \\`?$\\` 来预选 UI 元素。\n\nInspection ID: GrazieInspectionRunner" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "GrazieInspectionRunner", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "校对", + "index": 15, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "com.intellij.modules.json", + "version": "261.26222.73", + "rules": [ + { + "id": "JsonSchemaDeprecation", + "shortDescription": { + "text": "弃用的 JSON 属性" + }, + "fullDescription": { + "text": "报告 JSON 文件中弃用的属性。 请注意,JSON 架构规范中尚未定义弃用机制, 并且该检查使用了非标准的扩展 'deprecationMessage'。 Inspection ID: JsonSchemaDeprecation", + "markdown": "报告 JSON 文件中弃用的属性。 \n请注意,JSON 架构规范中尚未定义弃用机制, 并且该检查使用了非标准的扩展 'deprecationMessage'。\n\nInspection ID: JsonSchemaDeprecation" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "JsonSchemaDeprecation", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "JSON 和 JSON5", + "index": 16, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JsonSchemaRefReference", + "shortDescription": { + "text": "未解析的 '$ref' 和 '$schema' 引用" + }, + "fullDescription": { + "text": "报告 JSON 架构中未解析的 '$ref' 或 '$schema' 路径。 Inspection ID: JsonSchemaRefReference", + "markdown": "报告 JSON 架构中未解析的 `$ref` 或 `$schema` 路径。 \n\nInspection ID: JsonSchemaRefReference" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JsonSchemaRefReference", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JSON 和 JSON5", + "index": 16, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "Json5StandardCompliance", + "shortDescription": { + "text": "符合 JSON5 标准" + }, + "fullDescription": { + "text": "报告 JSON5 文件中语言规范不一致的情况。 Inspection ID: Json5StandardCompliance", + "markdown": "报告 JSON5 文件中[语言规范](http://json5.org)不一致的情况。\n\nInspection ID: Json5StandardCompliance" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "Json5StandardCompliance", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JSON 和 JSON5", + "index": 16, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JsonDuplicatePropertyKeys", + "shortDescription": { + "text": "对象字面量中的重复键" + }, + "fullDescription": { + "text": "报告对象字面量中的重复键。 Inspection ID: JsonDuplicatePropertyKeys", + "markdown": "报告对象字面量中的重复键。\n\nInspection ID: JsonDuplicatePropertyKeys" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JsonDuplicatePropertyKeys", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JSON 和 JSON5", + "index": 16, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JsonSchemaCompliance", + "shortDescription": { + "text": "符合 JSON 架构" + }, + "fullDescription": { + "text": "报告 JSON 文件与分配给它的 JSON 模式之间的不一致。 Inspection ID: JsonSchemaCompliance", + "markdown": "报告 JSON 文件与分配给它的 [JSON 模式](https://json-schema.org)之间的不一致。 \n\nInspection ID: JsonSchemaCompliance" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JsonSchemaCompliance", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JSON 和 JSON5", + "index": 16, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JsonStandardCompliance", + "shortDescription": { + "text": "符合 JSON 标准" + }, + "fullDescription": { + "text": "报告 JSON 文件与语言规范的以下差异: 行或块注释(可配置)。 多个顶层值(用于 JSON Lines 文件,可针对其他文件配置)。 对象或数组中的尾随逗号(可配置)。 用单引号引用的字符串。 属性键不是由双引号引用的字符串。 用 NaN 或正无穷大/负无穷大数值作为浮点字面量(可配置)。 Inspection ID: JsonStandardCompliance", + "markdown": "报告 JSON 文件与[语言规范](https://tools.ietf.org/html/rfc7159)的以下差异:\n\n* 行或块注释(可配置)。\n* 多个顶层值(用于 JSON Lines 文件,可针对其他文件配置)。\n* 对象或数组中的尾随逗号(可配置)。\n* 用单引号引用的字符串。\n* 属性键不是由双引号引用的字符串。\n* 用 NaN 或正无穷大/负无穷大数值作为浮点字面量(可配置)。\n\nInspection ID: JsonStandardCompliance" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "JsonStandardCompliance", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "JSON 和 JSON5", + "index": 16, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "com.intellij.database", + "version": "261.26222.73", + "rules": [ + { + "id": "MongoJSSideEffectsInspection", + "shortDescription": { + "text": "具有副作用的语句" + }, + "fullDescription": { + "text": "报告在数据源处于只读模式时可能导致副作用的语句。 有关启用只读模式的详细信息,请参阅 IDE 文档中的“为连接启用只读模式”。 禁用只读模式快速修复会关闭相应数据源的只读模式。 示例: 'db.my_collection.insertOne()' Inspection ID: MongoJSSideEffectsInspection", + "markdown": "报告在数据源处于只读模式时可能导致副作用的语句。\n\n有关启用只读模式的详细信息,请参阅 [IDE 文档中的\"为连接启用只读模式\"](https://www.jetbrains.com/help/datagrip/configuring-database-connections.html#enable-read-only-mode-for-a-connection)。\n\n**禁用只读模式**快速修复会关闭相应数据源的只读模式。\n\n示例:\n\n\n db.my_collection.insertOne()\n\nInspection ID: MongoJSSideEffectsInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MongoJSSideEffects", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "MongoJS", + "index": 17, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MysqlLoadDataPathInspection", + "shortDescription": { + "text": "LOAD 语句路径" + }, + "fullDescription": { + "text": "报告 LOAD 语句中以波浪符号开头的路径。 示例 (MySQL): 'CREATE TABLE table_name (id int);\nLOAD DATA LOCAL INFILE '~/Documents/some_file.txt'\nINTO TABLE table_name FIELDS TERMINATED BY ',' LINES TERMINATED BY '\\n'\nIGNORE 1 LINES;' 使用文件的完整路径代替波浪符号。 Inspection ID: MysqlLoadDataPathInspection", + "markdown": "报告 LOAD 语句中以波浪符号开头的路径。\n\n示例 (MySQL):\n\n CREATE TABLE table_name (id int);\n LOAD DATA LOCAL INFILE '~/Documents/some_file.txt'\n INTO TABLE table_name FIELDS TERMINATED BY ',' LINES TERMINATED BY '\\n'\n IGNORE 1 LINES;\n\n使用文件的完整路径代替波浪符号。\n\nInspection ID: MysqlLoadDataPathInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MysqlLoadDataPath", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "MySQL", + "index": 20, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MongoJSExtSideEffectsInspection", + "shortDescription": { + "text": "具有副作用的语句" + }, + "fullDescription": { + "text": "报告在数据源处于只读模式时可能导致副作用的语句。 该快速修复会关闭相应数据源的只读模式。 示例: 'db.my_collection.insertOne()' Inspection ID: MongoJSExtSideEffectsInspection", + "markdown": "报告在数据源处于只读模式时可能导致副作用的语句。\n\n该快速修复会关闭相应数据源的只读模式。\n\n示例:\n\n\n db.my_collection.insertOne()\n\nInspection ID: MongoJSExtSideEffectsInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MongoJSSideEffects", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "MongoJS", + "index": 17, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MysqlSpaceAfterFunctionNameInspection", + "shortDescription": { + "text": "函数名称和左圆括号之间的空格" + }, + "fullDescription": { + "text": "报告函数调用中函数名称和左圆括号之间的任何空格,这在默认情况下不受支持。 示例 (MySQL): 'SELECT MAX (qty) FROM orders;' Inspection ID: MysqlSpaceAfterFunctionNameInspection", + "markdown": "报告函数调用中函数名称和左圆括号之间的任何空格,这在默认情况下不受支持。\n\n示例 (MySQL):\n\n SELECT MAX (qty) FROM orders;\n\nInspection ID: MysqlSpaceAfterFunctionNameInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "MysqlSpaceAfterFunctionName", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "MySQL", + "index": 20, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlJoinCountInspection", + "shortDescription": { + "text": "JOIN 计数过多" + }, + "fullDescription": { + "text": "报告具有过多 JOIN 的查询。 出于性能原因,通常不建议使用过多的 JOIN。 'SELECT * FROM a inner join b using(id) inner join c using (id) inner join d using (id) inner join e using (id)' Inspection ID: SqlJoinCountInspection", + "markdown": "报告具有过多 JOIN 的查询。\n\n出于性能原因,通常不建议使用过多的 JOIN。\n\n SELECT * FROM a inner join b using(id) inner join c using (id) inner join d using (id) inner join e using (id)\n\nInspection ID: SqlJoinCountInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlJoinCount", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Performance" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlMissingReturnInspection", + "shortDescription": { + "text": "缺少 return 语句" + }, + "fullDescription": { + "text": "报告没有 RETURN 语句的函数。 示例 (Oracle): 'CREATE FUNCTION foo RETURN int AS\nBEGIN\nEND;' 'foo' 函数必须返回整数值,但函数体不返回任何内容。 要修正错误,请添加 RETURN 语句(例如,'RETURN 1;')。 'CREATE FUNCTION foo RETURN int AS\nBEGIN\n RETURN 1;\nEND;' Inspection ID: SqlMissingReturnInspection", + "markdown": "报告没有 RETURN 语句的函数。\n\n示例 (Oracle):\n\n CREATE FUNCTION foo RETURN int AS\n BEGIN\n END;\n\n`foo` 函数必须返回整数值,但函数体不返回任何内容。 要修正错误,请添加 RETURN 语句(例如,`RETURN 1;`)。\n\n CREATE FUNCTION foo RETURN int AS\n BEGIN\n RETURN 1;\n END;\n\nInspection ID: SqlMissingReturnInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "SqlMissingReturn", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlUnusedSubqueryItemInspection", + "shortDescription": { + "text": "未使用的子查询项" + }, + "fullDescription": { + "text": "报告外部查询表达式中未引用的列、别名和其他子查询项。 示例 (PostgreSQL): 'CREATE TABLE for_subquery(id INT);\nSELECT a, q FROM (SELECT 1 AS a, 10 AS b, 2 + 3 AS q, id\n FROM for_subquery) x;' 我们引用子查询中的别名 'a' 和 'q'。 但是 'b' 别名和 'id' 列在外部 SELECT 语句中没有被引用。 因此,'b' 和 'id' 将显示为灰色。 Inspection ID: SqlUnusedSubqueryItemInspection", + "markdown": "报告外部查询表达式中未引用的列、别名和其他子查询项。\n\n示例 (PostgreSQL):\n\n CREATE TABLE for_subquery(id INT);\n SELECT a, q FROM (SELECT 1 AS a, 10 AS b, 2 + 3 AS q, id\n FROM for_subquery) x;\n\n我们引用子查询中的别名 `a` 和 `q`。 但是 `b` 别名和 `id` 列在外部 SELECT 语句中没有被引用。 因此,`b` 和 `id` 将显示为灰色。\n\nInspection ID: SqlUnusedSubqueryItemInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlUnused", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlCaseVsIfInspection", + "shortDescription": { + "text": "使用 CASE 代替条件函数,反之亦然" + }, + "fullDescription": { + "text": "报告 CASE 和 IF 可互换的情况。 示例 (MySQL): 'SELECT CASE\nWHEN C1 IS NULL THEN 1\nELSE 0\nEND\nFROM dual;' 您可以通过用 IF 替换 CASE 结构来缩短代码。 为此,请应用 替换为 'IF' 调用 意图操作。 示例代码如下所示: 'SELECT IF(C1 IS NULL, 1, 0)\nFROM dual;' 要将 IF 恢复为 CASE,请点击 IF 并应用替换为 CASE 表达式 意图操作。 Inspection ID: SqlCaseVsIfInspection", + "markdown": "报告 CASE 和 IF 可互换的情况。\n\n示例 (MySQL):\n\n SELECT CASE\n WHEN C1 IS NULL THEN 1\n ELSE 0\n END\n FROM dual;\n\n您可以通过用 IF 替换 CASE 结构来缩短代码。 为此,请应用 **替换为 'IF' 调用** 意图操作。 示例代码如下所示:\n\n SELECT IF(C1 IS NULL, 1, 0)\n FROM dual;\n\n要将 IF 恢复为 CASE,请点击 IF 并应用**替换为 CASE 表达式** 意图操作。\n\nInspection ID: SqlCaseVsIfInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlCaseVsIf", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlShouldBeInGroupByInspection", + "shortDescription": { + "text": "列应在 group by 子句中" + }, + "fullDescription": { + "text": "报告不在 GROUP BY 子句或聚合函数调用中的列。 示例 (Microsoft SQL Server): 'CREATE TABLE t1 (a INT, b INT);\nSELECT a, b FROM t1 GROUP BY a;' 如果您运行 SELECT 查询,您将收到一个错误,因为 Microsoft SQL Server 需要 'b' 列在 GROUP BY 中或在聚合函数内使用。 以下两个示例将修复该错误。 'SELECT a, b FROM t1 GROUP BY a, b;\nSELECT a, max(b) max_b FROM t1 GROUP BY a;' Inspection ID: SqlShouldBeInGroupByInspection", + "markdown": "报告不在 GROUP BY 子句或聚合函数调用中的列。\n\n示例 (Microsoft SQL Server):\n\n CREATE TABLE t1 (a INT, b INT);\n SELECT a, b FROM t1 GROUP BY a;\n\n如果您运行 SELECT 查询,您将收到一个错误,因为 Microsoft SQL Server 需要 `b` 列在 GROUP BY 中或在聚合函数内使用。 以下两个示例将修复该错误。\n\n SELECT a, b FROM t1 GROUP BY a, b;\n SELECT a, max(b) max_b FROM t1 GROUP BY a;\n\nInspection ID: SqlShouldBeInGroupByInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlShouldBeInGroupBy", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlAutoIncrementDuplicateInspection", + "shortDescription": { + "text": "自动增量重复" + }, + "fullDescription": { + "text": "报告包含两个具有自动增量的列的表。 在 MySQL、Microsoft SQL Server 和 Db2 方言中,一个表只能有一个带有自动增量选项的字段,并且此字段必须是键。 示例 (MySQL): 'CREATE TABLE my_table\n(\n id INT AUTO_INCREMENT,\n c2 INT AUTO_INCREMENT,\n);' 'c2' 的 AUTO_INCREMENT 约束将被高亮显示,因为 'c1' 已经具有此约束。 要修正警告,您可以将 'id' 设为主键并删除 'c2' 的 AUTO_INCREMENT。 'CREATE TABLE my_table\n(\n id INT AUTO_INCREMENT PRIMARY KEY,\n c2 INT,\n);' Inspection ID: SqlAutoIncrementDuplicateInspection", + "markdown": "报告包含两个具有自动增量的列的表。 在 MySQL、Microsoft SQL Server 和 Db2 方言中,一个表只能有一个带有自动增量选项的字段,并且此字段必须是键。\n\n示例 (MySQL):\n\n CREATE TABLE my_table\n (\n id INT AUTO_INCREMENT,\n c2 INT AUTO_INCREMENT,\n );\n\n`c2` 的 AUTO_INCREMENT 约束将被高亮显示,因为 `c1` 已经具有此约束。 要修正警告,您可以将 `id` 设为主键并删除 `c2` 的 AUTO_INCREMENT。\n\n CREATE TABLE my_table\n (\n id INT AUTO_INCREMENT PRIMARY KEY,\n c2 INT,\n );\n\nInspection ID: SqlAutoIncrementDuplicateInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlAutoIncrementDuplicate", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlStringLengthExceededInspection", + "shortDescription": { + "text": "隐式字符串截断" + }, + "fullDescription": { + "text": "报告超过定义的字符长度的变量。 示例 (Microsoft SQL Server): 'CREATE PROCEDURE test() AS\nBEGIN\nDECLARE myVarOk VARCHAR(5) = 'abcde';\nDECLARE myVarExceeded VARCHAR(5) = 'abcde12345';\n\nSET myVarOk = 'xyz';\nSET myVarExceeded = '123456789';\nEND;' 'myVarExceeded' 变量被定义为 'VARCHAR(5)' 但两个分配的值(''abcde12345'' 和 ''123456789'') 都超出了此限制。 您可以截断指定的值或增加定义的长度。 要增加长度,请使用增加类型长度 快速修复。 在应用快速修复后: 'CREATE PROCEDURE test() AS\nBEGIN\nDECLARE myVarOk VARCHAR(5) = 'abcde';\nDECLARE myVarExceeded VARCHAR(10) = 'abcde12345';\n\nSET myVarOk = 'xyz';\nSET myVarExceeded = '123456789';\nEND;' Inspection ID: SqlStringLengthExceededInspection", + "markdown": "报告超过定义的字符长度的变量。\n\n示例 (Microsoft SQL Server):\n\n CREATE PROCEDURE test() AS\n BEGIN\n DECLARE myVarOk VARCHAR(5) = 'abcde';\n DECLARE myVarExceeded VARCHAR(5) = 'abcde12345';\n\n SET myVarOk = 'xyz';\n SET myVarExceeded = '123456789';\n END;\n\n`myVarExceeded` 变量被定义为 `VARCHAR(5)` 但两个分配的值(`'abcde12345'` 和 `'123456789'`) 都超出了此限制。 您可以截断指定的值或增加定义的长度。\n要增加长度,请使用**增加类型长度** 快速修复。\n\n在应用快速修复后:\n\n CREATE PROCEDURE test() AS\n BEGIN\n DECLARE myVarOk VARCHAR(5) = 'abcde';\n DECLARE myVarExceeded VARCHAR(10) = 'abcde12345';\n\n SET myVarOk = 'xyz';\n SET myVarExceeded = '123456789';\n END;\n\nInspection ID: SqlStringLengthExceededInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlStringLengthExceeded", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlSideEffectsInspection", + "shortDescription": { + "text": "具有副作用的语句" + }, + "fullDescription": { + "text": "报告在只读连接期间可能会修改数据库的语句。 要为连接启用只读模式,请在数据库工具窗口(视图 | 工具窗口 | 数据库)中右键点击数据源,然后选择属性。 在数据源和驱动程序对话框中,点击选项标签页并选中只读复选框。 示例 (MySQL): 'CREATE TABLE foo(a INT);\nINSERT INTO foo VALUES (1);' 由于 'CREATE TABLE' 和'INSERT INTO' 语句会导致数据库修改,因此这些语句将在只读连接模式中高亮显示。 Inspection ID: SqlSideEffectsInspection", + "markdown": "报告在只读连接期间可能会修改数据库的语句。\n\n要为连接启用只读模式,请在**数据库** 工具窗口(**视图 \\| 工具窗口 \\| 数据库** )中右键点击数据源,然后选择**属性** 。\n在**数据源和驱动程序** 对话框中,点击**选项** 标签页并选中**只读**复选框。\n\n示例 (MySQL):\n\n CREATE TABLE foo(a INT);\n INSERT INTO foo VALUES (1);\n\n由于 `CREATE TABLE` 和`INSERT INTO` 语句会导致数据库修改,因此这些语句将在只读连接模式中高亮显示。\n\nInspection ID: SqlSideEffectsInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlSideEffects", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlDtInspection", + "shortDescription": { + "text": "格式错误的日期/时间字面量" + }, + "fullDescription": { + "text": "报告日期和时间字面量中的错误。 此检查在 MySQL、Oracle、Db2 和 H2 中可用。 示例 (MySQL): 'SELECT TIME '10 -12:13:14' FROM dual;\nSELECT TIME ' 12 : 13 : 14 ' FROM dual;\nSELECT TIME '12 13 14' FROM dual;\nSELECT TIME '12-13-14' FROM dual;\nSELECT TIME '12.13.14' FROM dual;\nSELECT TIME '12:13:' FROM dual;\nSELECT TIME '12:13' FROM dual;\nSELECT TIME '12:' FROM dual;' 在此示例中,日期会忽略 MySQL 日期字面量和时间字面量标准。 因此,这些将高亮显示。 有关 MySQL 中日期和时间字面量的详细信息,请参阅 dev.mysql.com 上的 Date and Time Literals。 以下日期和时间字面量对 MySQL 有效。 'SELECT TIME '12:13:14' FROM dual;\nSELECT TIME '12:13:14.555' FROM dual;\nSELECT TIME '12:13:14.' FROM dual;\nSELECT TIME '-12:13:14' FROM dual;\nSELECT TIME '10 12:13:14' FROM dual;\nSELECT TIME '-10 12:13:14' FROM dual;' Inspection ID: SqlDtInspection", + "markdown": "报告日期和时间字面量中的错误。 此检查在 MySQL、Oracle、Db2 和 H2 中可用。\n\n示例 (MySQL):\n\n SELECT TIME '10 -12:13:14' FROM dual;\n SELECT TIME ' 12 : 13 : 14 ' FROM dual;\n SELECT TIME '12 13 14' FROM dual;\n SELECT TIME '12-13-14' FROM dual;\n SELECT TIME '12.13.14' FROM dual;\n SELECT TIME '12:13:' FROM dual;\n SELECT TIME '12:13' FROM dual;\n SELECT TIME '12:' FROM dual;\n\n在此示例中,日期会忽略 MySQL 日期字面量和时间字面量标准。 因此,这些将高亮显示。\n有关 MySQL 中日期和时间字面量的详细信息,请参阅 [dev.mysql.com 上的 Date and Time Literals](https://dev.mysql.com/doc/refman/8.0/en/date-and-time-literals.html)。\n\n以下日期和时间字面量对 MySQL 有效。\n\n SELECT TIME '12:13:14' FROM dual;\n SELECT TIME '12:13:14.555' FROM dual;\n SELECT TIME '12:13:14.' FROM dual;\n SELECT TIME '-12:13:14' FROM dual;\n SELECT TIME '10 12:13:14' FROM dual;\n SELECT TIME '-10 12:13:14' FROM dual;\n\nInspection ID: SqlDtInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlDateTime", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlIllegalCursorStateInspection", + "shortDescription": { + "text": "非法光标状态" + }, + "fullDescription": { + "text": "报告 SQL 例程中的非法光标状态。 例程具有 CLOSE 或 FETCH 语句,但光标可能已被关闭。 例程具有 OPEN 语句,但光标可能已被打开。 示例 (Microsoft SQL Server): 'CREATE TABLE t(col INT);\n\nCREATE PROCEDURE foo() AS\nBEGIN\nDECLARE my_cursor CURSOR FOR SELECT * FROM t;\nDECLARE a INT;\nFETCH my_cursor INTO a;\nCLOSE my_cursor;\nEND;' 根据 docs.microsoft.com 上的 CLOSE (Transact-SQL),必须为打开的光标执行 CLOSE,并且不允许对仅声明或已经关闭的光标执行 CLOSE。 因此,我们需要打开光标来修正警告。 'CREATE PROCEDURE foo() AS\nBEGIN\nDECLARE my_cursor CURSOR FOR SELECT * FROM t;\nDECLARE a INT;\nOPEN my_cursor;\nFETCH my_cursor INTO a;\nCLOSE my_cursor;\nEND;' Inspection ID: SqlIllegalCursorStateInspection", + "markdown": "报告 SQL 例程中的非法光标状态。\n\n* 例程具有 CLOSE 或 FETCH 语句,但光标可能已被关闭。\n* 例程具有 OPEN 语句,但光标可能已被打开。\n\n示例 (Microsoft SQL Server):\n\n CREATE TABLE t(col INT);\n\n CREATE PROCEDURE foo() AS\n BEGIN\n DECLARE my_cursor CURSOR FOR SELECT * FROM t;\n DECLARE a INT;\n FETCH my_cursor INTO a;\n CLOSE my_cursor;\n END;\n\n根据 [docs.microsoft.com 上的 CLOSE (Transact-SQL)](https://docs.microsoft.com/en-us/sql/t-sql/language-elements/close-transact-sql),必须为打开的光标执行 CLOSE,并且不允许对仅声明或已经关闭的光标执行 CLOSE。 因此,我们需要打开光标来修正警告。\n\n CREATE PROCEDURE foo() AS\n BEGIN\n DECLARE my_cursor CURSOR FOR SELECT * FROM t;\n DECLARE a INT;\n OPEN my_cursor;\n FETCH my_cursor INTO a;\n CLOSE my_cursor;\n END;\n\nInspection ID: SqlIllegalCursorStateInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlIllegalCursorState", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlJoinWithoutOnInspection", + "shortDescription": { + "text": "'delete' 语句中的 'join' 子句不安全" + }, + "fullDescription": { + "text": "报告可能更改整个数据库的语句的缺失条件检查。 例如,在没有 ON 或 WHERE 的 DELETE 语句中使用 JOIN 子句。 如果不对 JOIN 进行条件检查,DELETE 将删除整个表的内容。 示例 (MySQL): 'CREATE TABLE foo (a INT,b INT,c INT);\nCREATE TABLE bar (a INT,b INT,c INT);\n\nDELETE table1 FROM foo table1 INNER JOIN bar table2;' Inspection ID: SqlJoinWithoutOnInspection", + "markdown": "报告可能更改整个数据库的语句的缺失条件检查。\n\n例如,在没有 ON 或 WHERE 的 DELETE 语句中使用 JOIN 子句。 如果不对 JOIN 进行条件检查,DELETE 将删除整个表的内容。\n\n示例 (MySQL):\n\n CREATE TABLE foo (a INT,b INT,c INT);\n CREATE TABLE bar (a INT,b INT,c INT);\n\n DELETE table1 FROM foo table1 INNER JOIN bar table2;\n\nInspection ID: SqlJoinWithoutOnInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlJoinWithoutOn", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlDropIndexedColumnInspection", + "shortDescription": { + "text": "索引依赖于列" + }, + "fullDescription": { + "text": "报告尝试从索引表中删除列的情况。 此检查在 Microsoft SQL Server 和 Sybase ASE 中可用。 示例 (Microsoft SQL Server): 'CREATE TABLE test_index\n(\ncol INT NOT NULL,\ncol2 INT NOT NULL,\ncol3 INT NOT NULL UNIQUE,\ncol4 VARCHAR(200)\n);\n\nCREATE UNIQUE INDEX aaaa ON test_index (col, col2);\n\nALTER TABLE test_index\nDROP COLUMN col;' 您不能删除 'col' 列,因为它在索引表中。 要删除该列,您需要先删除 'aaaa' 索引(例如 DROP INDEX aaaa)。 Inspection ID: SqlDropIndexedColumnInspection", + "markdown": "报告尝试从索引表中删除列的情况。 此检查在 Microsoft SQL Server 和 Sybase ASE 中可用。\n\n示例 (Microsoft SQL Server):\n\n CREATE TABLE test_index\n (\n col INT NOT NULL,\n col2 INT NOT NULL,\n col3 INT NOT NULL UNIQUE,\n col4 VARCHAR(200)\n );\n\n CREATE UNIQUE INDEX aaaa ON test_index (col, col2);\n\n ALTER TABLE test_index\n DROP COLUMN col;\n\n您不能删除 `col` 列,因为它在索引表中。 要删除该列,您需要先删除 `aaaa` 索引(例如 DROP INDEX aaaa)。\n\nInspection ID: SqlDropIndexedColumnInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlDropIndexedColumn", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlCheckUsingColumnsInspection", + "shortDescription": { + "text": "检查 using 子句列" + }, + "fullDescription": { + "text": "报告两个表中都不存在的 USING 子句中的列。 示例 (MySQL): 'CREATE TABLE t1 (i INT, j INT);\nCREATE TABLE t2 (k INT, l INT);\nSELECT * FROM t1 JOIN t2 USING (j);' 在 USING 子句中,列名必须存在于两个表中,SELECT 查询将使用给定的列名自动连接这些表。 由于我们在 't2' 中没有 'j' 列,我们可以使用 ON 重写查询。 ON 子句可以连接两个表中列名不匹配的表。 'SELECT * FROM t1 JOIN t2 ON t1.j = t2.l;' Inspection ID: SqlCheckUsingColumnsInspection", + "markdown": "报告两个表中都不存在的 USING 子句中的列。\n\n示例 (MySQL):\n\n CREATE TABLE t1 (i INT, j INT);\n CREATE TABLE t2 (k INT, l INT);\n SELECT * FROM t1 JOIN t2 USING (j);\n\n在 USING 子句中,列名必须存在于两个表中,SELECT 查询将使用给定的列名自动连接这些表。 由于我们在 `t2` 中没有 `j` 列,我们可以使用 ON 重写查询。 ON 子句可以连接两个表中列名不匹配的表。\n\n SELECT * FROM t1 JOIN t2 ON t1.j = t2.l;\n\nInspection ID: SqlCheckUsingColumnsInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlCheckUsingColumns", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlInsertValuesInspection", + "shortDescription": { + "text": "VALUE 子句基数" + }, + "fullDescription": { + "text": "报告 VALUES 中的形参数目与目标表中的列数不匹配的情况。 示例 (MySQL): 'CREATE TABLE foo(a INT, b INT, c INT);\n\nINSERT INTO foo VALUES (1,2,3,4)' 'foo' 表有三列,但在 INSERT INTO 语句中我们传递了四列。 Inspection ID: SqlInsertValuesInspection", + "markdown": "报告 VALUES 中的形参数目与目标表中的列数不匹配的情况。\n\n示例 (MySQL):\n\n CREATE TABLE foo(a INT, b INT, c INT);\n\n INSERT INTO foo VALUES (1,2,3,4)\n\n`foo` 表有三列,但在 INSERT INTO 语句中我们传递了四列。\n\nInspection ID: SqlInsertValuesInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlInsertValues", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlDialectInspection", + "shortDescription": { + "text": "SQL 方言检测" + }, + "fullDescription": { + "text": "报告未将方言分配给 SQL 文件的情况。 例如,当您打开一个新的 SQL 文件而没有为其分配方言时,您会看到一条通知,其中建议了最匹配的方言。 点击 使用 链接以使用建议的方言。 或者,点击将方言更改为链接以选择其他方言。 Inspection ID: SqlDialectInspection", + "markdown": "报告未将方言分配给 SQL 文件的情况。\n\n例如,当您打开一个新的 SQL 文件而没有为其分配方言时,您会看到一条通知,其中建议了最匹配的方言。 点击 **使用\\** 链接以使用建议的方言。 或者,点击**将方言更改为**链接以选择其他方言。\n\nInspection ID: SqlDialectInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlDialectInspection", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlRedundantElseNullInspection", + "shortDescription": { + "text": "冗余 ELSE NULL 子句" + }, + "fullDescription": { + "text": "报告冗余的 ELSE NULL 子句。 示例 (MySQL): 'SELECT CASE WHEN 2 > 1 THEN 'OK' ELSE NULL END AS alias FROM foo;' 'ELSE NULL' 部分永远不会被执行,可以省略。 Inspection ID: SqlRedundantElseNullInspection", + "markdown": "报告冗余的 ELSE NULL 子句。\n\n示例 (MySQL):\n\n SELECT CASE WHEN 2 > 1 THEN 'OK' ELSE NULL END AS alias FROM foo;\n\n`ELSE NULL` 部分永远不会被执行,可以省略。\n\nInspection ID: SqlRedundantElseNullInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlRedundantElseNull", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MysqlParsingInspection", + "shortDescription": { + "text": "8.0 以前的版本中不受支持的语法" + }, + "fullDescription": { + "text": "报告查询中 UNION 的无效用法。 该检查适用于早于 8.0 的 MySQL 版本。 示例 (MySQL): 'SELECT * FROM (SELECT 1 UNION (SELECT 1 UNION SELECT 2)) a;' Inspection ID: MysqlParsingInspection", + "markdown": "报告查询中 UNION 的无效用法。\n\n该检查适用于早于 8.0 的 MySQL 版本。\n\n示例 (MySQL):\n\n\n SELECT * FROM (SELECT 1 UNION (SELECT 1 UNION SELECT 2)) a;\n\nInspection ID: MysqlParsingInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MysqlParsing", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "MySQL", + "index": 20, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlCallNotationInspection", + "shortDescription": { + "text": "使用命名实参和位置实参" + }, + "fullDescription": { + "text": "报告位置实参在命名实参之后的调用。 适用于 PostgreSQL、Oracle 和 Db2。 示例(PostgreSQL 中): 'CREATE FUNCTION foo(a int, b int, c int) RETURNS int\n LANGUAGE plpgsql AS\n$$\nBEGIN\n RETURN a + b + c;\nEND\n$$;\nSELECT foo(a => 1, b => 2, c => 3);\n -- `3` 在命名实参之后\nSELECT foo(1, b => 2, 3);\n -- `1` 和 `3` 在命名实参之后\nSELECT foo(b => 2, 1, 3);' Inspection ID: SqlCallNotationInspection", + "markdown": "报告位置实参在命名实参之后的调用。 适用于 PostgreSQL、Oracle 和 Db2。\n\n示例(PostgreSQL 中):\n\n CREATE FUNCTION foo(a int, b int, c int) RETURNS int\n LANGUAGE plpgsql AS\n $$\n BEGIN\n RETURN a + b + c;\n END\n $$;\n SELECT foo(a => 1, b => 2, c => 3);\n -- `3` 在命名实参之后\n SELECT foo(1, b => 2, 3);\n -- `1` 和 `3` 在命名实参之后\n SELECT foo(b => 2, 1, 3);\n\nInspection ID: SqlCallNotationInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "SqlCallNotation", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MongoJSExtDeprecationInspection", + "shortDescription": { + "text": "弃用的元素" + }, + "fullDescription": { + "text": "报告 MongoDB 和 JavaScript 代码中已弃用的方法的用法。 快速修复用推荐的替代方法替换了已弃用的方法。 示例: 'db.my_collection.insert()' 在应用快速修复后: 'db.my_collection.insertOne()' Inspection ID: MongoJSExtDeprecationInspection", + "markdown": "报告 MongoDB 和 JavaScript 代码中已弃用的方法的用法。\n\n快速修复用推荐的替代方法替换了已弃用的方法。\n\n示例:\n\n\n db.my_collection.insert()\n\n在应用快速修复后:\n\n\n db.my_collection.insertOne()\n\nInspection ID: MongoJSExtDeprecationInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MongoJSDeprecation", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "MongoJS", + "index": 17, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlDuplicateColumnInspection", + "shortDescription": { + "text": "SELECT 中的重复列名" + }, + "fullDescription": { + "text": "报告 SELECT 列表中列别名的重复名称。 示例(Sybase ASE): 'CREATE TABLE t1 (a TEXT, b INT, c INT);\n\nSELECT a AS x, b AS x FROM t1;' 'x' 别名用于 'a' 和 'b' 列。 这些分配高亮显示为错误,因为您不能对 Sybase ASE 中的列使用相同的别名。 Inspection ID: SqlDuplicateColumnInspection", + "markdown": "报告 SELECT 列表中列别名的重复名称。\n\n示例(Sybase ASE):\n\n CREATE TABLE t1 (a TEXT, b INT, c INT);\n\n SELECT a AS x, b AS x FROM t1;\n\n`x` 别名用于 `a` 和 `b` 列。 这些分配高亮显示为错误,因为您不能对 Sybase ASE 中的列使用相同的别名。\n\nInspection ID: SqlDuplicateColumnInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlDuplicateColumn", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlShadowingAliasInspection", + "shortDescription": { + "text": "列被别名隐藏" + }, + "fullDescription": { + "text": "报告 SELECT 中别名名称与 FROM 子句中的列名称匹配。 示例 (MySQL): 'CREATE TABLE foo (a INT, b INT, c INT);\nSELECT a b, c FROM foo;' 'a' 列使用 'b' 别名,但 'b' 名称也被 'foo' 表中的列使用。 Inspection ID: SqlShadowingAliasInspection", + "markdown": "报告 SELECT 中别名名称与 FROM 子句中的列名称匹配。\n\n示例 (MySQL):\n\n CREATE TABLE foo (a INT, b INT, c INT);\n SELECT a b, c FROM foo;\n\n`a` 列使用 `b` 别名,但 `b` 名称也被 `foo` 表中的列使用。\n\nInspection ID: SqlShadowingAliasInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlShadowingAlias", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MongoJSDeprecationInspection", + "shortDescription": { + "text": "弃用的元素" + }, + "fullDescription": { + "text": "报告 MongoDB 和 JavaScript 代码中已弃用的方法的用法。 快速修复会将已弃用的方法替换为推荐的替代方法。 示例: 'db.my_collection.insert()' 在应用快速修复后: 'db.my_collection.insertOne()' Inspection ID: MongoJSDeprecationInspection", + "markdown": "报告 MongoDB 和 JavaScript 代码中已弃用的方法的用法。\n\n快速修复会将已弃用的方法替换为推荐的替代方法。\n\n示例:\n\n db.my_collection.insert()\n\n在应用快速修复后:\n\n db.my_collection.insertOne()\n\nInspection ID: MongoJSDeprecationInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MongoJSDeprecation", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "MongoJS", + "index": 17, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlTypeInspection", + "shortDescription": { + "text": "类型兼容性" + }, + "fullDescription": { + "text": "报告与类型相关的错误。 Inspection ID: SqlTypeInspection", + "markdown": "报告与类型相关的错误。\n\nInspection ID: SqlTypeInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlType", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlUnreachableCodeInspection", + "shortDescription": { + "text": "不可到达的代码" + }, + "fullDescription": { + "text": "报告 SQL 例程中不可到达的语句。 示例 (Microsoft SQL Server): 'CREATE FUNCTION foo() RETURNS INT AS\nBEGIN\n THROW;\n RETURN 1;\nEND;' 在 Microsoft SQL Server 中,'THROW' 语句会抛出异常并将执行转移到 TRY...CATCH 结构的 CATCH 块。 因此,'RETURN 1;' 部分将永远不会被执行。 Inspection ID: SqlUnreachableCodeInspection", + "markdown": "报告 SQL 例程中不可到达的语句。\n\n示例 (Microsoft SQL Server):\n\n CREATE FUNCTION foo() RETURNS INT AS\n BEGIN\n THROW;\n RETURN 1;\n END;\n\n在 Microsoft SQL Server 中,`THROW` 语句会抛出异常并将执行转移到 TRY...CATCH 结构的 CATCH 块。 因此,`RETURN 1;` 部分将永远不会被执行。\n\nInspection ID: SqlUnreachableCodeInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlUnreachable", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlUnicodeStringLiteralInspection", + "shortDescription": { + "text": "SQL 中的 Unicode 用法" + }, + "fullDescription": { + "text": "报告使用没有 'N' 前缀的国家字符的字符串字面量。 如果没有 N 前缀,字符串将被转换为数据库的默认代码页面。 此默认代码页面可能无法识别某些字符。 有关更多信息,请参阅 docs.microsoft.com 上的“nchar 和 nvarchar”(Transact-SQL) 页面。 示例 (Microsoft SQL Server): 'SELECT 'abcde' AS a;\nSELECT N'abcde' AS b;\nSELECT 'абвгд' AS c;\nSELECT N'абвгд' AS d;' 'SELECT 'абвгд' AS c;' 没有 'N' 前缀,''абвгд'' 部分将被高亮显示。 Inspection ID: SqlUnicodeStringLiteralInspection", + "markdown": "报告使用没有 `N` 前缀的国家字符的字符串字面量。\n\n如果没有 N 前缀,字符串将被转换为数据库的默认代码页面。 此默认代码页面可能无法识别某些字符。 有关更多信息,请参阅 [docs.microsoft.com 上的\"nchar 和 nvarchar\"(Transact-SQL)](https://docs.microsoft.com/en-us/sql/t-sql/data-types/nchar-and-nvarchar-transact-sql) 页面。\n\n示例 (Microsoft SQL Server):\n\n SELECT 'abcde' AS a;\n SELECT N'abcde' AS b;\n SELECT 'абвгд' AS c;\n SELECT N'абвгд' AS d;\n\n`SELECT 'абвгд' AS c;` 没有 `N` 前缀,`'абвгд'` 部分将被高亮显示。\n\nInspection ID: SqlUnicodeStringLiteralInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlUnicodeStringLiteral", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlSingleSessionModeInspection", + "shortDescription": { + "text": "创建没有单会话模式的临时表" + }, + "fullDescription": { + "text": "报告未在单会话模式下进行的临时表创建。 示例 (PostgreSQL): 'CREATE TEMPORARY TABLE foo(a INT, b INT);' Inspection ID: SqlSingleSessionModeInspection", + "markdown": "报告未在单会话模式下进行的临时表创建。\n\n示例 (PostgreSQL):\n\n CREATE TEMPORARY TABLE foo(a INT, b INT);\n\nInspection ID: SqlSingleSessionModeInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlSingleSessionMode", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlUnusedVariableInspection", + "shortDescription": { + "text": "未使用的变量" + }, + "fullDescription": { + "text": "报告未使用的实参、变量或形参。 示例 (PostgreSQL): 'CREATE FUNCTION foo(PARAMUSED INT, PARAMUNUSED INT) RETURNS INT AS\n$$\nBEGIN\n RETURN PARAMUSED;\nEND\n$$ LANGUAGE plpgsql;' 'PARAMUNUSED' 形参未在函数中使用,可能会被删除。 Inspection ID: SqlUnusedVariableInspection", + "markdown": "报告未使用的实参、变量或形参。\n\n示例 (PostgreSQL):\n\n CREATE FUNCTION foo(PARAMUSED INT, PARAMUNUSED INT) RETURNS INT AS\n $$\n BEGIN\n RETURN PARAMUSED;\n END\n $$ LANGUAGE plpgsql;\n\n`PARAMUNUSED` 形参未在函数中使用,可能会被删除。\n\nInspection ID: SqlUnusedVariableInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlUnused", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlNoDataSourceInspection", + "shortDescription": { + "text": "未配置数据源" + }, + "fullDescription": { + "text": "报告数据库工具窗口中缺少数据源(视图 | 工具窗口 | 数据库)。 Inspection ID: SqlNoDataSourceInspection", + "markdown": "报告**数据库** 工具窗口中缺少数据源(**视图 \\| 工具窗口 \\| 数据库** )。\n\nInspection ID: SqlNoDataSourceInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlNoDataSourceInspection", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlResolveInspection", + "shortDescription": { + "text": "未解析的引用" + }, + "fullDescription": { + "text": "报告未解析的 SQL 引用。 示例 (MySQL): 'CREATE TABLE users(id INT, name VARCHAR(40));\nCREATE TABLE admins(id INT, col1 INT);\n\nSELECT users.id, admins.id FROM admins WHERE admins.id > 1;' 'users.id' 列未解析,因为 FROM 子句中缺少 'users' 表。 Inspection ID: SqlResolveInspection", + "markdown": "报告未解析的 SQL 引用。\n\n示例 (MySQL):\n\n CREATE TABLE users(id INT, name VARCHAR(40));\n CREATE TABLE admins(id INT, col1 INT);\n\n SELECT users.id, admins.id FROM admins WHERE admins.id > 1;\n\n`users.id` 列未解析,因为 FROM 子句中缺少 `users` 表。\n\nInspection ID: SqlResolveInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "SqlResolve", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "PgSelectFromProcedureInspection", + "shortDescription": { + "text": "Postgres: 从过程调用中 select" + }, + "fullDescription": { + "text": "报告从函数执行 SELECT 或执行没有类型别名的 DBLINK(例如 'AS t1 (s VARCHAR)')的情况。 此要求不适用于标量函数。 示例 (PostgreSQL): 'CREATE FUNCTION produce_a_table() RETURNS RECORD AS $$\nSELECT 1;\n$$ LANGUAGE sql;\nSELECT * FROM produce_a_table() AS s (c1 INT);\nSELECT * FROM produce_a_table() AS s (c1);\nSELECT * FROM DBLINK('dbname=mydb', 'SELECT proname, prosrc FROM pg_proc') AS t1;' 'AS s (c1 INT)' 具有类型别名,而 'AS s (c1)' 和 'AS t1' 没有。 在这种情况下,将高亮显示 'produce_a_table()' 和 'DBLINK()' 的第二次调用。 Inspection ID: PgSelectFromProcedureInspection", + "markdown": "报告从函数执行 SELECT 或执行没有类型别名的 DBLINK(例如 ` AS t1 (s VARCHAR) `)的情况。\n\n此要求不适用于标量函数。\n\n示例 (PostgreSQL):\n\n CREATE FUNCTION produce_a_table() RETURNS RECORD AS $$\n SELECT 1;\n $$ LANGUAGE sql;\n SELECT * FROM produce_a_table() AS s (c1 INT);\n SELECT * FROM produce_a_table() AS s (c1);\n SELECT * FROM DBLINK('dbname=mydb', 'SELECT proname, prosrc FROM pg_proc') AS t1;\n\n`AS s (c1 INT)` 具有类型别名,而 `AS s (c1)` 和 `AS t1` 没有。\n在这种情况下,将高亮显示 `produce_a_table()` 和 `DBLINK()` 的第二次调用。\n\nInspection ID: PgSelectFromProcedureInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "PgSelectFromProcedure", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "PostgreSQL", + "index": 68, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlCurrentSchemaInspection", + "shortDescription": { + "text": "当前查询文件架构已内省" + }, + "fullDescription": { + "text": "报告当前会话中未自省的架构和数据库。 例如,当您尝试在未内省的架构中创建表时,可能会出现此警告。 内省是一种检查数据源的方法。 执行内省时,将从数据库中检索结构信息以检测各种对象及其特性。例如,可能是表、列、函数和其他元素。 Inspection ID: SqlCurrentSchemaInspection", + "markdown": "报告当前会话中未自省的架构和数据库。\n\n例如,当您尝试在未内省的架构中创建表时,可能会出现此警告。\n\n内省是一种检查数据源的方法。 执行内省时,将从数据库中检索结构信息以检测各种对象及其特性。例如,可能是表、列、函数和其他元素。\n\nInspection ID: SqlCurrentSchemaInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlCurrentSchemaInspection", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlMisleadingReferenceInspection", + "shortDescription": { + "text": "误导性引用" + }, + "fullDescription": { + "text": "报告 SQL 代码中不明确的引用。 例如,当名称同时引用表列和例程形参时。 由于违反直观的解析逻辑,此类代码的执行可能会导致错误或意外结果。 通常,范围更局部的名称具有更高的优先级。 示例 (PostgreSQL): 'CREATE TABLE foo\n(\n id INT,\n name VARCHAR(5)\n);\nCREATE FUNCTION func(name VARCHAR(5)) RETURNS INT AS\n$$\nDECLARE\n b INT;\nBEGIN\n -- `name` 不明确,因为它被用作列名和形参\n SELECT COUNT(*) INTO b FROM foo t WHERE t.name = name;\n RETURN b;\nEND;\n$$ LANGUAGE plpgsql;' 在 PostgreSQL 中,您可以使用 '#variable_conflict' 指令来明确指定正确的引用。 例如,使用 '#variable_conflict use_column' 引用列名,或使用 '#variable_conflict use_variable' 引用形参。 'CREATE TABLE foo\n(\n id INT,\n name VARCHAR(5)\n);\nCREATE FUNCTION func(name VARCHAR(5)) RETURNS INT AS\n$$\n #variable_conflict use_column\nDECLARE\n b INT;\nBEGIN\n SELECT COUNT(*) INTO b FROM foo t WHERE t.name = name;\n RETURN b;\nEND;\n$$ LANGUAGE plpgsql;' Inspection ID: SqlMisleadingReferenceInspection", + "markdown": "报告 SQL 代码中不明确的引用。\n\n例如,当名称同时引用表列和例程形参时。 由于违反直观的解析逻辑,此类代码的执行可能会导致错误或意外结果。 通常,范围更局部的名称具有更高的优先级。\n\n示例 (PostgreSQL):\n\n CREATE TABLE foo\n (\n id INT,\n name VARCHAR(5)\n );\n CREATE FUNCTION func(name VARCHAR(5)) RETURNS INT AS\n $$\n DECLARE\n b INT;\n BEGIN\n -- `name` 不明确,因为它被用作列名和形参\n SELECT COUNT(*) INTO b FROM foo t WHERE t.name = name;\n RETURN b;\n END;\n $$ LANGUAGE plpgsql;\n\n在 PostgreSQL 中,您可以使用 `#variable_conflict` 指令来明确指定正确的引用。 例如,使用 `#variable_conflict use_column` 引用列名,或使用 `#variable_conflict use_variable` 引用形参。\n\n CREATE TABLE foo\n (\n id INT,\n name VARCHAR(5)\n );\n CREATE FUNCTION func(name VARCHAR(5)) RETURNS INT AS\n $$\n #variable_conflict use_column\n DECLARE\n b INT;\n BEGIN\n SELECT COUNT(*) INTO b FROM foo t WHERE t.name = name;\n RETURN b;\n END;\n $$ LANGUAGE plpgsql;\n\nInspection ID: SqlMisleadingReferenceInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlMisleadingReference", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlInsertIntoGeneratedColumnInspection", + "shortDescription": { + "text": "插入到生成的列中" + }, + "fullDescription": { + "text": "报告将值赋给生成的列的 INSERT 语句。 可以读取生成的列,但不能直接写入其值。 示例 (PostgreSQL): 'CREATE TABLE foo\n(\n col1 INT,\n col2 INT GENERATED ALWAYS AS (col1 + 1) STORED\n);\nINSERT INTO foo(col1, col2) VALUES (1, 2);'\n 您不能将 '2' 插入到 'col2' 列中,因为此列已生成。 要使此脚本正常工作,您可以将 '2' 更改为 DEFAULT。 'INSERT INTO foo(col1, col2) VALUES (1, DEFAULT);' Inspection ID: SqlInsertIntoGeneratedColumnInspection", + "markdown": "报告将值赋给生成的列的 INSERT 语句。 可以读取生成的列,但不能直接写入其值。\n\n示例 (PostgreSQL):\n\n CREATE TABLE foo\n (\n col1 INT,\n col2 INT GENERATED ALWAYS AS (col1 + 1) STORED\n );\n INSERT INTO foo(col1, col2) VALUES (1, 2);\n\n您不能将 `2` 插入到 `col2` 列中,因为此列已生成。\n要使此脚本正常工作,您可以将 `2` 更改为 DEFAULT。\n`INSERT INTO foo(col1, col2) VALUES (1, DEFAULT);`\n\nInspection ID: SqlInsertIntoGeneratedColumnInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlInsertIntoGeneratedColumn", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlRedundantLimitInspection", + "shortDescription": { + "text": "查询中的冗余行限制" + }, + "fullDescription": { + "text": "报告查询中限制行的冗余子句,例如 FETCH 和 LIMIT。 示例 (PostgreSQL): 'CREATE TABLE foo(a INT);\n\nSELECT * FROM foo WHERE EXISTS(SELECT * FROM foo LIMIT 2);\nSELECT * FROM foo WHERE EXISTS(SELECT * FROM foo FETCH FIRST 2 ROWS ONLY);' 要修复警告,您可以将 OFFSET 添加到限制子句。 如果缺少 OFFSET,则 LIMIT 是多余的,因为 LIMIT 的用法不会影响 EXISTS 的运算结果。 在 OFFSET 的情况下,我们跳过前 'N' 行,这将影响输出。 'SELECT * FROM foo WHERE EXISTS(SELECT * FROM foo OFFSET 1 ROW LIMIT 2);\nSELECT * FROM foo WHERE EXISTS(SELECT * FROM foo OFFSET 1 ROW FETCH FIRST 2 ROWS ONLY);' Inspection ID: SqlRedundantLimitInspection", + "markdown": "报告查询中限制行的冗余子句,例如 FETCH 和 LIMIT。\n\n示例 (PostgreSQL):\n\n CREATE TABLE foo(a INT);\n\n SELECT * FROM foo WHERE EXISTS(SELECT * FROM foo LIMIT 2);\n SELECT * FROM foo WHERE EXISTS(SELECT * FROM foo FETCH FIRST 2 ROWS ONLY);\n\n要修复警告,您可以将 OFFSET 添加到限制子句。 如果缺少 OFFSET,则 LIMIT 是多余的,因为 LIMIT 的用法不会影响 EXISTS 的运算结果。 在 OFFSET 的情况下,我们跳过前 `N` 行,这将影响输出。\n\n SELECT * FROM foo WHERE EXISTS(SELECT * FROM foo OFFSET 1 ROW LIMIT 2);\n SELECT * FROM foo WHERE EXISTS(SELECT * FROM foo OFFSET 1 ROW FETCH FIRST 2 ROWS ONLY);\n\nInspection ID: SqlRedundantLimitInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlRedundantLimit", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlInsertNullIntoNotNullInspection", + "shortDescription": { + "text": "将 NULL 插入 NOT NULL 列" + }, + "fullDescription": { + "text": "报告将 NULL 值插入仅接受 NOT NULL 值的列的情况。 示例 (Microsoft SQL Server): 'CREATE TABLE br2 (\nid INT NOT NULL,\ncol1 NVARCHAR (20) NOT NULL,\ncol2 NVARCHAR (20) NOT NULL,\n);\n--\nINSERT INTO br2 (id, col1, col2)\nVALUES (1, NULL, NULL);' 您不能在 'col1' 和 'col2' 中插入 NULL 值,因为它们被定义为 NOT NULL。 如果按原样运行脚本,则会收到错误消息。 要修复此代码,请将 VALUES 部分中的 NULL 替换为值(例如,'42' 和 ''bird'')。 INSERT INTO br2 (id, col1, col2)\nVALUES (1, 42, 'bird'); Inspection ID: SqlInsertNullIntoNotNullInspection", + "markdown": "报告将 NULL 值插入仅接受 NOT NULL 值的列的情况。\n\n示例 (Microsoft SQL Server):\n\n CREATE TABLE br2 (\n id INT NOT NULL,\n col1 NVARCHAR (20) NOT NULL,\n col2 NVARCHAR (20) NOT NULL,\n );\n --\n INSERT INTO br2 (id, col1, col2)\n VALUES (1, NULL, NULL);\n\n您不能在 `col1` 和 `col2` 中插入 NULL 值,因为它们被定义为 NOT NULL。 如果按原样运行脚本,则会收到错误消息。 要修复此代码,请将 VALUES 部分中的 NULL 替换为值(例如,`42` 和 `'bird'`)。\n\n```\nINSERT INTO br2 (id, col1, col2)\nVALUES (1, 42, 'bird');\n```\n\nInspection ID: SqlInsertNullIntoNotNullInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlInsertNullIntoNotNull", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlDerivedTableAliasInspection", + "shortDescription": { + "text": "每个派生表都应具有别名" + }, + "fullDescription": { + "text": "报告没有别名的派生表。 示例 (MySQL): 'CREATE TABLE table1 (id INT, name VARCHAR(20), cats FLOAT);\nCREATE TABLE table2 (id INT, age INTEGER);\n\nSELECT id AS ID, name, cats, age\nFROM (SELECT table1.id, name, cats, age\nFROM table1\nJOIN table2 ON table1.id = table2.id);' 根据 dev.mysql.com 上的 Derived Tables,别名是强制的。 您可以使用引入别名快速修复来添加别名。 在应用快速修复后: 'SELECT id AS ID, name, cats, age\nFROM (SELECT table1.id, name, cats, age\nFROM table1\nJOIN table2 ON table1.id = table2.id);' Inspection ID: SqlDerivedTableAliasInspection", + "markdown": "报告没有别名的派生表。\n\n示例 (MySQL):\n\n CREATE TABLE table1 (id INT, name VARCHAR(20), cats FLOAT);\n CREATE TABLE table2 (id INT, age INTEGER);\n\n SELECT id AS ID, name, cats, age\n FROM (SELECT table1.id, name, cats, age\n FROM table1\n JOIN table2 ON table1.id = table2.id);\n\n根据 [dev.mysql.com 上的 Derived Tables](https://dev.mysql.com/doc/refman/8.0/en/derived-tables.html),别名是强制的。 您可以使用**引入别名**快速修复来添加别名。\n\n在应用快速修复后:\n\n SELECT id AS ID, name, cats, age\n FROM (SELECT table1.id, name, cats, age\n FROM table1\n JOIN table2 ON table1.id = table2.id);\n\nInspection ID: SqlDerivedTableAliasInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlDerivedTableAlias", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MsBuiltinInspection", + "shortDescription": { + "text": "内置函数" + }, + "fullDescription": { + "text": "报告 ISNULL 函数中字符串实参的截断。 ISNULL 语法是 'ISNULL(check_expression, replacement_value)'。 根据 docs.microsoft.com 上的 ISNULL,如果 'replacement_value' 比 'check_expression' 长,'replacement_value' 将被截断。 示例 (Microsoft SQL Server): 'DECLARE @name1 VARCHAR(2) = NULL;\nDECLARE @name2 VARCHAR(10) = 'Example';\nDECLARE @name3 VARCHAR(2) = 'Hi';\n\n -- `@name2` 是 VARCHAR(10) 并且将被截断\nSELECT ISNULL(@name1, @name2);\n\n -- `@name3` 是 VARCHAR(2)(作为 `@name1`)并且不会被截断\nSELECT ISNULL(@name1, @name3);' Inspection ID: MsBuiltinInspection", + "markdown": "报告 ISNULL 函数中字符串实参的截断。\n\nISNULL 语法是 `ISNULL(check_expression, replacement_value)`。\n\n根据 [docs.microsoft.com 上的 ISNULL](https://docs.microsoft.com/en-us/sql/t-sql/functions/isnull-transact-sql),如果 `replacement_value` 比 `check_expression` 长,`replacement_value` 将被截断。\n\n示例 (Microsoft SQL Server):\n\n DECLARE @name1 VARCHAR(2) = NULL;\n DECLARE @name2 VARCHAR(10) = 'Example';\n DECLARE @name3 VARCHAR(2) = 'Hi';\n\n -- `@name2` 是 VARCHAR(10) 并且将被截断\n SELECT ISNULL(@name1, @name2);\n\n -- `@name3` 是 VARCHAR(2)(作为 `@name1`)并且不会被截断\n SELECT ISNULL(@name1, @name3);\n\nInspection ID: MsBuiltinInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MssqlBuiltin", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "SQL Server", + "index": 69, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlTriggerTransitionInspection", + "shortDescription": { + "text": "触发器中的可疑代码" + }, + "fullDescription": { + "text": "报告触发器中转换表变量的不正确用法。 示例 (HSQLDB): 'CREATE TABLE foo(a INT);\n\nCREATE TRIGGER trg\n AFTER DELETE ON foo\nBEGIN\n SELECT * FROM NEW;\nEND;\n\nCREATE TRIGGER trig AFTER INSERT ON foo\n REFERENCING OLD ROW AS newrow\n FOR EACH ROW WHEN (a > 1)\n INSERT INTO foo VALUES (1)' 在 HSQLDB 中,DELETE 触发器只能用于 OLD 状态,而 INSERT 触发器只能用于 NEW 状态。 因此,在前面的示例中,'SELECT * FROM NEW;' 中的 NEW 和 'REFERENCING OLD ROW AS newrow' 中的 OLD 将被高亮显示。 Inspection ID: SqlTriggerTransitionInspection", + "markdown": "报告触发器中转换表变量的不正确用法。\n\n示例 (HSQLDB):\n\n CREATE TABLE foo(a INT);\n\n CREATE TRIGGER trg\n AFTER DELETE ON foo\n BEGIN\n SELECT * FROM NEW;\n END;\n\n CREATE TRIGGER trig AFTER INSERT ON foo\n REFERENCING OLD ROW AS newrow\n FOR EACH ROW WHEN (a > 1)\n INSERT INTO foo VALUES (1)\n\n在 HSQLDB 中,DELETE 触发器只能用于 OLD 状态,而 INSERT 触发器只能用于 NEW 状态。 因此,在前面的示例中,`SELECT * FROM NEW;` 中的 NEW 和 `REFERENCING OLD ROW AS newrow` 中的 OLD 将被高亮显示。\n\nInspection ID: SqlTriggerTransitionInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlTriggerTransition", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlMultipleLimitClausesInspection", + "shortDescription": { + "text": "查询中的多个行限制/偏移子句" + }, + "fullDescription": { + "text": "报告单个查询中多个行限制子句的使用情况。 示例(Microsoft SQL 服务器): 'create table foo(a int);\nselect top 1 * from foo order by a offset 10 rows fetch next 20 rows only;' SELECT TOP 子句用于指定必须只返回 1 条记录。 FETCH 子句指定在处理完 OFFSET 子句后要返回的行数。 但是因为我们已经有了 SELECT TOP 限制子句,所以 FETCH 子句可能是冗余的。 Inspection ID: SqlMultipleLimitClausesInspection", + "markdown": "报告单个查询中多个行限制子句的使用情况。\n\n示例(Microsoft SQL 服务器):\n\n create table foo(a int);\n select top 1 * from foo order by a offset 10 rows fetch next 20 rows only;\n\nSELECT TOP 子句用于指定必须只返回 1 条记录。 FETCH 子句指定在处理完 OFFSET 子句后要返回的行数。 但是因为我们已经有了 SELECT TOP 限制子句,所以 FETCH 子句可能是冗余的。\n\nInspection ID: SqlMultipleLimitClausesInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlMultipleLimitClauses", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlNamedArgumentsInspection", + "shortDescription": { + "text": "应使用命名实参" + }, + "fullDescription": { + "text": "报告在例程调用中不带名称使用的实参。 默认情况下,此检查处于禁用状态。 有关命名形参和未命名形参之间差异的详细信息,请参阅 docs.microsoft.com 上的 Binding Parameters by Name (Named Parameters) 。 示例 (Microsoft SQL Server): 'CREATE FUNCTION foo(n INT, m INT) RETURNS INT AS\nBEGIN\n RETURN n + m;\nEND;\n\nCREATE PROCEDURE test AS\nBEGIN\n foo n = 1, m = 2;\n\n--- 以下调用缺少形参名称,会被高亮显示\n foo 1, 2;\nEND;' 'foo 1, 2;' 调用中的形参 '1, 2' 突出显示,因为它们缺少名称。 Inspection ID: SqlNamedArgumentsInspection", + "markdown": "报告在例程调用中不带名称使用的实参。 默认情况下,此检查处于禁用状态。\n\n有关命名形参和未命名形参之间差异的详细信息,请参阅 [docs.microsoft.com 上的 Binding Parameters by Name (Named Parameters)](https://docs.microsoft.com/en-us/sql/odbc/reference/develop-app/binding-parameters-by-name-named-parameters)。\n\n示例 (Microsoft SQL Server):\n\n CREATE FUNCTION foo(n INT, m INT) RETURNS INT AS\n BEGIN\n RETURN n + m;\n END;\n\n CREATE PROCEDURE test AS\n BEGIN\n foo n = 1, m = 2;\n\n --- 以下调用缺少形参名称,会被高亮显示\n foo 1, 2;\n END;\n\n`foo 1, 2;` 调用中的形参 `1, 2` 突出显示,因为它们缺少名称。\n\nInspection ID: SqlNamedArgumentsInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlNamedArguments", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlCaseVsCoalesceInspection", + "shortDescription": { + "text": "使用 CASE 代替 COALESCE 函数,反之亦然" + }, + "fullDescription": { + "text": "报告 CASE 和 COALESCE 调用可以互换的情况。 此检查具有以下意图操作:替换为 'COALESCE' 调用和相反的 替换为 CASE 表达式。 示例 (MySQL): 'SELECT\n -- this CASE may be replaced by COALESCE\n\tCASE\n\t\tWHEN C1 IS NOT NULL THEN C1\n\t\tELSE 0\n\t\tEND\nFROM dual;' 在该示例中,CASE 语句可以替换为 'SELECT COALESCE(C1, 0)',这会产生相同的结果。 如果您更喜欢使用 CASE 表达式,请在检查页面上选择 CASE 表达式优先于 COALESCE 函数选项。 Inspection ID: SqlCaseVsCoalesceInspection", + "markdown": "报告 CASE 和 COALESCE 调用可以互换的情况。 此检查具有以下意图操作:**替换为 'COALESCE' 调用** 和相反的 **替换为 CASE 表达式** 。\n\n示例 (MySQL):\n\n SELECT\n -- this CASE may be replaced by COALESCE\n \tCASE\n \t\tWHEN C1 IS NOT NULL THEN C1\n \t\tELSE 0\n \t\tEND\n FROM dual;\n\n在该示例中,CASE 语句可以替换为 `SELECT COALESCE(C1, 0)`,这会产生相同的结果。\n\n如果您更喜欢使用 CASE 表达式,请在检查页面上选择 **CASE 表达式优先于 COALESCE 函数**选项。\n\nInspection ID: SqlCaseVsCoalesceInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlCaseVsCoalesce", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlRedundantAliasInspection", + "shortDescription": { + "text": "冗余别名表达式" + }, + "fullDescription": { + "text": "报告可能与表中的列名重叠并且可能是冗余的别名表达式。 示例 (PostgreSQL): 'CREATE TABLE foo(a INT, b INT);\n\nSELECT * FROM foo foo(a, b);\nSELECT * FROM foo foo(a);\nSELECT * FROM foo foo(x);\nSELECT * FROM foo foo(x, y);' 前两个别名使用与 'foo' 表中相同的列名。 这些被认为是冗余的,因为列具有相同的名称。 Inspection ID: SqlRedundantAliasInspection", + "markdown": "报告可能与表中的列名重叠并且可能是冗余的别名表达式。\n\n示例 (PostgreSQL):\n\n CREATE TABLE foo(a INT, b INT);\n\n SELECT * FROM foo foo(a, b);\n SELECT * FROM foo foo(a);\n SELECT * FROM foo foo(x);\n SELECT * FROM foo foo(x, y);\n\n前两个别名使用与 `foo` 表中相同的列名。 这些被认为是冗余的,因为列具有相同的名称。\n\nInspection ID: SqlRedundantAliasInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlRedundantAlias", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlIdentifierInspection", + "shortDescription": { + "text": "标识符应加引号" + }, + "fullDescription": { + "text": "报告在查询中使用 SQL 预留关键字作为标识符名称时的情况。 示例 (Microsoft SQL Server): 'CREATE TABLE select (identity INT IDENTITY NOT NULL, order INT NOT NULL);' 我们使用 'select'、'identity' 和'order' 作为表名和列名。 但它们也是 Microsoft SQL Server 中的保留关键字。 因此,为了将它们用作查询中的对象名称,您必须用引号引用这些标识符。 要用引号引用它们,您可以使用为标识符加引号 快速修复。 在应用快速修复后: 'CREATE TABLE [select] ([identity] INT IDENTITY NOT NULL, [order] INT NOT NULL);' Inspection ID: SqlIdentifierInspection", + "markdown": "报告在查询中使用 SQL 预留关键字作为标识符名称时的情况。\n\n示例 (Microsoft SQL Server):\n\n CREATE TABLE select (identity INT IDENTITY NOT NULL, order INT NOT NULL);\n\n我们使用 `select`、`identity` 和`order` 作为表名和列名。\n但它们也是 Microsoft SQL Server 中的保留关键字。\n因此,为了将它们用作查询中的对象名称,您必须用引号引用这些标识符。 要用引号引用它们,您可以使用**为标识符加引号** 快速修复。\n\n在应用快速修复后:\n\n CREATE TABLE [select] ([identity] INT IDENTITY NOT NULL, [order] INT NOT NULL);\n\nInspection ID: SqlIdentifierInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlIdentifier", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlTransactionStatementInTriggerInspection", + "shortDescription": { + "text": "在触发器中使用事务管理语句" + }, + "fullDescription": { + "text": "报告触发器主体中事务管理语句(如 COMMIT 或 ROLLBACK)的用法。 在触发器主体中使用 COMMIT 或 ROLLBACK 语句时,触发器将不会编译。 发生错误是因为触发器在事务中间触发。 当触发器启动时,当前事务仍未完成。 因为 COMMIT 会终止事务,这两个语句(COMMIT 和 ROLLBACK)都会导致异常。 在触发器中执行的更改应该由启动触发器的所属事务提交(或回滚)。 示例(Oracle): 'CREATE TABLE employee_audit\n(\n id INT NOT NULL,\n update_date DATE NOT NULL,\n old_name VARCHAR2(100),\n new_name VARCHAR2(100)\n);\n\nCREATE TABLE employees\n(\n id INT NOT NULL,\n name VARCHAR2(100) NOT NULL\n);\n\nCREATE OR REPLACE TRIGGER trig_commit\n AFTER UPDATE OF name\n ON employees\n FOR EACH ROW\nBEGIN\n INSERT INTO employee_audit VALUES (:old.id, SYSDATE, :old.name, :new.name);\n COMMIT;\nEND;\n\nCREATE OR REPLACE TRIGGER trig_rollback\n AFTER UPDATE OF name\n ON employees\n FOR EACH ROW\nBEGIN\n INSERT INTO employee_audit VALUES (:old.id, SYSDATE, :old.name, :new.name);\n ROLLBACK;\nEND;' Inspection ID: SqlTransactionStatementInTriggerInspection", + "markdown": "报告触发器主体中事务管理语句(如 COMMIT 或 ROLLBACK)的用法。\n\n在触发器主体中使用 COMMIT 或 ROLLBACK 语句时,触发器将不会编译。\n发生错误是因为触发器在事务中间触发。 当触发器启动时,当前事务仍未完成。 因为 COMMIT 会终止事务,这两个语句(COMMIT 和 ROLLBACK)都会导致异常。\n在触发器中执行的更改应该由启动触发器的所属事务提交(或回滚)。\n\n示例(Oracle):\n\n CREATE TABLE employee_audit\n (\n id INT NOT NULL,\n update_date DATE NOT NULL,\n old_name VARCHAR2(100),\n new_name VARCHAR2(100)\n );\n\n CREATE TABLE employees\n (\n id INT NOT NULL,\n name VARCHAR2(100) NOT NULL\n );\n\n CREATE OR REPLACE TRIGGER trig_commit\n AFTER UPDATE OF name\n ON employees\n FOR EACH ROW\n BEGIN\n INSERT INTO employee_audit VALUES (:old.id, SYSDATE, :old.name, :new.name);\n COMMIT;\n END;\n\n CREATE OR REPLACE TRIGGER trig_rollback\n AFTER UPDATE OF name\n ON employees\n FOR EACH ROW\n BEGIN\n INSERT INTO employee_audit VALUES (:old.id, SYSDATE, :old.name, :new.name);\n ROLLBACK;\n END;\n\nInspection ID: SqlTransactionStatementInTriggerInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlTransactionStatementInTrigger", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlRedundantCodeInCoalesceInspection", + "shortDescription": { + "text": "COALESCE 调用中的冗余代码" + }, + "fullDescription": { + "text": "报告除 COALESCE 函数中第一个不评估为 NULL 的表达式之外的所有实参。 示例 (MySQL): 'SELECT COALESCE(NULL, NULL, NULL, 42, NULL, 'string') as a;' 第一个 NOT NULL 实参是 '42',所有其他实参将显示为灰色。 Inspection ID: SqlRedundantCodeInCoalesceInspection", + "markdown": "报告除 COALESCE 函数中第一个不评估为 NULL 的表达式之外的所有实参。\n\n示例 (MySQL):\n\n SELECT COALESCE(NULL, NULL, NULL, 42, NULL, 'string') as a;\n\n第一个 NOT NULL 实参是 `42`,所有其他实参将显示为灰色。\n\nInspection ID: SqlRedundantCodeInCoalesceInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlRedundantCodeInCoalesce", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlStorageInspection", + "shortDescription": { + "text": "SQL 源修改检测" + }, + "fullDescription": { + "text": "报告数据库对象的源代码已更改的情况。 执行数据库或对象内省时会触发检查。 当您打开对象的源代码、运行语句和执行代码重构时,就会运行内省。 此外,您可以通过右键点击对象并选择刷新来运行内省。 检查包括以下几种情况: 数据库中的对象源代码已更改,但编辑器中的代码未更新。 适用于 PostgreSQL、Microsoft SQL Server、Oracle 和 Sybase ASE。 您更改了对象源代码,内省了数据库,但源代码已被其他人更改。 IDE 中的数据库内省器已更新,您需要下载先前内省器版本中缺少的新对象属性。 Inspection ID: SqlStorageInspection", + "markdown": "报告数据库对象的源代码已更改的情况。\n\n执行数据库或对象内省时会触发检查。 当您打开对象的源代码、运行语句和执行代码重构时,就会运行内省。\n此外,您可以通过右键点击对象并选择**刷新**来运行内省。\n\n检查包括以下几种情况:\n\n* 数据库中的对象源代码已更改,但编辑器中的代码未更新。 适用于 PostgreSQL、Microsoft SQL Server、Oracle 和 Sybase ASE。\n* 您更改了对象源代码,内省了数据库,但源代码已被其他人更改。\n* IDE 中的数据库内省器已更新,您需要下载先前内省器版本中缺少的新对象属性。\n\nInspection ID: SqlStorageInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlStorageInspection", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MsOrderByInspection", + "shortDescription": { + "text": "查询中的 ORDER BY" + }, + "fullDescription": { + "text": "报告在视图、内联函数、派生表、子查询和通用表表达式中使用 'ORDER BY' 子句而不使用 'TOP'、'OFFSET' 或 'FOR XML' 的用法。 有关 'ORDER BY' 用法的详细信息,请参阅 docs.microsoft.com 上的 SELECT - ORDER BY 子句 (Transact-SQL)。 示例(Microsoft SQL Server): 'CREATE TABLE foo (a INT NOT NULL, b INT NOT NULL);\n\nSELECT *\nFROM (SELECT a, b\nFROM foo A\nWHERE a < 89\nORDER BY b) ALIAS;' 在子查询中,ORDER BY 将被高亮显示为错误。 您可以将 TOP、OFFSET 或 FOR XML 添加到子查询。 或者,使用删除元素快速修复来删除 ORDER BY 部分。 在应用快速修复后: 'SELECT *\nFROM (SELECT a, b\nFROM foo A\nWHERE a < 89) ALIAS;' Inspection ID: MsOrderByInspection", + "markdown": "报告在视图、内联函数、派生表、子查询和通用表表达式中使用 `ORDER BY` 子句而不使用 `TOP`、`OFFSET` 或 `FOR XML` 的用法。\n\n有关 `ORDER BY` 用法的详细信息,请参阅 [docs.microsoft.com 上的 SELECT - ORDER BY 子句 (Transact-SQL)](https://docs.microsoft.com/en-us/sql/t-sql/queries/select-order-by-clause-transact-sql)。\n\n示例(Microsoft SQL Server):\n\n CREATE TABLE foo (a INT NOT NULL, b INT NOT NULL);\n\n SELECT *\n FROM (SELECT a, b\n FROM foo A\n WHERE a < 89\n ORDER BY b) ALIAS;\n\n在子查询中,ORDER BY 将被高亮显示为错误。 您可以将 TOP、OFFSET 或 FOR XML 添加到子查询。\n或者,使用**删除元素**快速修复来删除 ORDER BY 部分。\n\n在应用快速修复后:\n\n SELECT *\n FROM (SELECT a, b\n FROM foo A\n WHERE a < 89) ALIAS;\n\nInspection ID: MsOrderByInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "MsOrderBy", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "SQL Server", + "index": 69, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlSignatureInspection", + "shortDescription": { + "text": "函数签名" + }, + "fullDescription": { + "text": "报告内置函数的签名问题。 检查将报告错误数量的实参、无效的关键字、错误的数据类型和其他问题。 示例(MySQL): 'CREATE TABLE foo (a INT, b INT, c INT)\n\nSELECT IFNULL() FROM foo; -- 错误\nSELECT IFNULL(a) FROM foo; -- 错误\nSELECT IFNULL(a, b) FROM foo; -- 正确\nSELECT IFNULL(a, b, c) FROM foo; -- 错误' 在 MySQL 中,'IFNULL()' 函数严格接受两个实参。 因此,只有 'SELECT IFNULL(a, b) FROM foo;' 查询是正确的。 Inspection ID: SqlSignatureInspection", + "markdown": "报告内置函数的签名问题。\n\n检查将报告错误数量的实参、无效的关键字、错误的数据类型和其他问题。\n\n示例(MySQL):\n\n CREATE TABLE foo (a INT, b INT, c INT)\n\n SELECT IFNULL() FROM foo; -- 错误\n SELECT IFNULL(a) FROM foo; -- 错误\n SELECT IFNULL(a, b) FROM foo; -- 正确\n SELECT IFNULL(a, b, c) FROM foo; -- 错误\n\n在 MySQL 中,`IFNULL()` 函数严格接受两个实参。 因此,只有 `SELECT IFNULL(a, b) FROM foo;` 查询是正确的。\n\nInspection ID: SqlSignatureInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlSignature", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlRedundantOrderingDirectionInspection", + "shortDescription": { + "text": "冗余排序方向" + }, + "fullDescription": { + "text": "在 ORDER BY 子句中报告冗余的排序方向,如 ASC 和 DESC。 示例 (MySQL): 'CREATE TABLE foo(a INT, b INT, c INT);\nSELECT * FROM foo ORDER BY a ASC, b DESC, c ASC;' ORDER BY 关键字默认按升序对记录进行排序。 因此,'a' 和 'c' 列的 'ASC' 关键字是冗余的。 Inspection ID: SqlRedundantOrderingDirectionInspection", + "markdown": "在 ORDER BY 子句中报告冗余的排序方向,如 ASC 和 DESC。\n\n示例 (MySQL):\n\n CREATE TABLE foo(a INT, b INT, c INT);\n SELECT * FROM foo ORDER BY a ASC, b DESC, c ASC;\n\nORDER BY 关键字默认按升序对记录进行排序。 因此,`a` 和 `c` 列的 `ASC` 关键字是冗余的。\n\nInspection ID: SqlRedundantOrderingDirectionInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlRedundantOrderingDirection", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlDeprecateTypeInspection", + "shortDescription": { + "text": "弃用的类型" + }, + "fullDescription": { + "text": "报告已弃用并且可能在 DBMS 的未来版本中消失的类型的用法。 报告的类型: Oracle 中的 LONG(请参阅 docs.oracle.com 上的 Deprecated and Desupported Features 页面)。 Microsoft SQL Server 中的 TEXT、NTEXT 和 IMAGE(请参阅 docs.microsoft.com 上的 Deprecated Database Engine Features in SQL Server 2016 页面)。 示例 (Oracle): 'CREATE TABLE ot.foo(\na NUMBER GENERATED BY DEFAULT AS IDENTITY,\nb LONG NOT NULL\n);' Inspection ID: SqlDeprecateTypeInspection", + "markdown": "报告已弃用并且可能在 DBMS 的未来版本中消失的类型的用法。\n\n报告的类型:\n\n* Oracle 中的 LONG(请参阅 [docs.oracle.com 上的 Deprecated and Desupported Features](https://docs.oracle.com/cd/A91202_01/901_doc/server.901/a90120/ch4_dep.htm#6690) 页面)。\n* Microsoft SQL Server 中的 TEXT、NTEXT 和 IMAGE(请参阅 [docs.microsoft.com 上的 Deprecated Database Engine Features in SQL Server 2016](https://docs.microsoft.com/en-us/sql/database-engine/deprecated-database-engine-features-in-sql-server-2016?view=sql-server-ver15) 页面)。\n\n示例 (Oracle):\n\n CREATE TABLE ot.foo(\n a NUMBER GENERATED BY DEFAULT AS IDENTITY,\n b LONG NOT NULL\n );\n\nInspection ID: SqlDeprecateTypeInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlDeprecateType", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlAggregatesInspection", + "shortDescription": { + "text": "与聚合相关的问题" + }, + "fullDescription": { + "text": "报告 SQL 聚合函数的无效用法。 即以下情况: 在 HAVING 和 ORDER BY 子句中使用但在 GROUP BY 子句中遗漏的列。 'CREATE TABLE foo(id INT PRIMARY KEY, a INT, b INT);\nSELECT a, MAX(b) FROM foo GROUP BY a HAVING b > 0;\nSELECT * FROM foo GROUP BY a ORDER BY b;' 当按主键进行分组时,此规则不适用。 'SELECT * FROM foo GROUP BY id ORDER BY b;' 在错误的上下文中聚合函数。 通常,您可以在以下上下文中使用聚合函数: SELECT 中的表达式列表; 在 HAVING 和 ORDER BY 部分; 和其他特定于方言的案例。 以下查询将显示错误。 'SELECT a FROM foo WHERE MAX(b) > 0;\nSELECT a FROM foo GROUP BY MAX(a);' 聚合函数的嵌套调用。 'SELECT MAX(SUM(a)) FROM foo GROUP BY a;' 此规则不适用于解析函数。 以下查询有效且正确。 'SELECT MAX(SUM(a) OVER ()) FROM foo;' 没有聚合函数的 HAVING 的用法。 在这种情况下,请考虑使用 WHERE 部分重写您的代码。 'SELECT a, MAX(b) FROM foo GROUP BY a HAVING a > 0;' Inspection ID: SqlAggregatesInspection", + "markdown": "报告 SQL 聚合函数的无效用法。\n\n即以下情况:\n\n* 在 HAVING 和 ORDER BY 子句中使用但在 GROUP BY 子句中遗漏的列。\n\n CREATE TABLE foo(id INT PRIMARY KEY, a INT, b INT);\n SELECT a, MAX(b) FROM foo GROUP BY a HAVING b > 0;\n SELECT * FROM foo GROUP BY a ORDER BY b;\n\n 当按主键进行分组时,此规则不适用。\n\n SELECT * FROM foo GROUP BY id ORDER BY b;\n\n* 在错误的上下文中聚合函数。 通常,您可以在以下上下文中使用聚合函数: SELECT 中的表达式列表; 在 HAVING 和 ORDER BY 部分; 和其他特定于方言的案例。 以下查询将显示错误。\n\n SELECT a FROM foo WHERE MAX(b) > 0;\n SELECT a FROM foo GROUP BY MAX(a);\n\n* 聚合函数的嵌套调用。\n\n SELECT MAX(SUM(a)) FROM foo GROUP BY a;\n\n 此规则不适用于解析函数。 以下查询有效且正确。\n\n SELECT MAX(SUM(a) OVER ()) FROM foo;\n\n* 没有聚合函数的 HAVING 的用法。 在这种情况下,请考虑使用 WHERE 部分重写您的代码。\n\n SELECT a, MAX(b) FROM foo GROUP BY a HAVING a > 0;\n\nInspection ID: SqlAggregatesInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlAggregates", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlConstantExpressionInspection", + "shortDescription": { + "text": "常量表达式" + }, + "fullDescription": { + "text": "报告始终为 true、false 或 null 的条件和表达式。 示例 (MySQL): 'CREATE TABLE t1 (a TEXT, b INT, c BOOLEAN);\nSELECT a FROM t1 WHERE 'Cat' = 'Cat';\nSELECT a FROM t1 WHERE 'Cat' = null;' ''Cat' = 'Cat'' 始终为 true,将被报告。 ''Cat' = null' 始终为 null,将被报告。 Inspection ID: SqlConstantExpressionInspection", + "markdown": "报告始终为 true、false 或 null 的条件和表达式。\n\n示例 (MySQL):\n\n CREATE TABLE t1 (a TEXT, b INT, c BOOLEAN);\n SELECT a FROM t1 WHERE 'Cat' = 'Cat';\n SELECT a FROM t1 WHERE 'Cat' = null;\n\n`'Cat' = 'Cat'` 始终为 true,将被报告。\n\n`'Cat' = null` 始终为 null,将被报告。\n\nInspection ID: SqlConstantExpressionInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlConstantExpression", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlMissingColumnAliasesInspection", + "shortDescription": { + "text": "缺少列别名" + }, + "fullDescription": { + "text": "报告在输出表达式中没有显式别名的查询(例如,在 SELECT 语句中)。 示例(PostgreSQL): 'CREATE TABLE foo(a INT, b INT);\n\nSELECT 1, a + 1 AS A2, MAX(b) AS M\nFROM foo;' Inspection ID: SqlMissingColumnAliasesInspection", + "markdown": "报告在输出表达式中没有显式别名的查询(例如,在 SELECT 语句中)。\n\n示例(PostgreSQL):\n\n CREATE TABLE foo(a INT, b INT);\n\n SELECT 1, a + 1 AS A2, MAX(b) AS M\n FROM foo;\n\nInspection ID: SqlMissingColumnAliasesInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlMissingColumnAliases", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlAddNotNullColumnInspection", + "shortDescription": { + "text": "添加没有默认值的非 null 列" + }, + "fullDescription": { + "text": "报告创建没有 DEFAULT 值的 NOT NULL 列的尝试。 示例 (Microsoft SQL Server): 'CREATE TABLE foo (a INT, b INT)\n\nALTER TABLE foo ADD c INT NOT NULL;' 默认情况下,列包含 NULL 值。 在示例中,我们使用 NOT NULL 约束强制列不接受 NULL 值。 如果我们禁止使用 NULL 值,则必须设置 SQL 在创建新记录时可以使用的 DEFAULT 值。 'ALTER TABLE foo ADD c INT NOT NULL DEFAULT 42;' 您可以使用添加 DEFAULT 值快速修复快速添加 DEFAULT 值。 Inspection ID: SqlAddNotNullColumnInspection", + "markdown": "报告创建没有 DEFAULT 值的 NOT NULL 列的尝试。\n\n示例 (Microsoft SQL Server):\n\n CREATE TABLE foo (a INT, b INT)\n\n ALTER TABLE foo ADD c INT NOT NULL;\n\n默认情况下,列包含 NULL 值。 在示例中,我们使用 NOT NULL 约束强制列不接受 NULL 值。\n如果我们禁止使用 NULL 值,则必须设置 SQL 在创建新记录时可以使用的 DEFAULT 值。\n\n ALTER TABLE foo ADD c INT NOT NULL DEFAULT 42;\n\n您可以使用**添加 DEFAULT 值**快速修复快速添加 DEFAULT 值。\n\nInspection ID: SqlAddNotNullColumnInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlAddNotNullColumn", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "OraOverloadInspection", + "shortDescription": { + "text": "重载错误" + }, + "fullDescription": { + "text": "报告 Oracle 中子程序重载的无效情况。 示例 (Oracle): 'DECLARE\n SUBTYPE fff IS BINARY_INTEGER;\n SUBTYPE ggg IS NATURAL;\n PROCEDURE foo (a IN ggg) IS BEGIN NULL; END;\n PROCEDURE foo (a IN fff) IS BEGIN NULL; END;\nBEGIN\n NULL;\nEND;' 您不能重载形参仅在子类型方面不同的子程序。 例如,您不能重载一个接受 BINARY INTEGER 形参而另一个接受 NATURAL 形参的过程。 有关过程重载限制的更多信息,请参阅 docs.oracle.com 上的 Restrictions on Overloading。 Inspection ID: OraOverloadInspection", + "markdown": "报告 Oracle 中子程序重载的无效情况。\n\n示例 (Oracle):\n\n DECLARE\n SUBTYPE fff IS BINARY_INTEGER;\n SUBTYPE ggg IS NATURAL;\n PROCEDURE foo (a IN ggg) IS BEGIN NULL; END;\n PROCEDURE foo (a IN fff) IS BEGIN NULL; END;\n BEGIN\n NULL;\n END;\n\n您不能重载形参仅在子类型方面不同的子程序。 例如,您不能重载一个接受 BINARY INTEGER 形参而另一个接受 NATURAL 形参的过程。 有关过程重载限制的更多信息,请参阅 [docs.oracle.com 上的 Restrictions on Overloading](https://docs.oracle.com/cd/B19306_01/appdev.102/b14261/subprograms.htm)。\n\nInspection ID: OraOverloadInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlOverload", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Oracle", + "index": 73, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "OraMissingBodyInspection", + "shortDescription": { + "text": "缺少软件包/对象类型规范的主体" + }, + "fullDescription": { + "text": "报告缺少主体声明的软件包和对象类型规范。 声明例程的软件包规范和对象类型以及带有光标的软件包规范必须具有实现这些例程和光标的主体声明。 在程序代码中调用例程或光标时,缺少主体会导致运行时错误。 示例 (Oracle): 'CREATE OR REPLACE PACKAGE ppp IS\n FUNCTION foo(a INT) RETURN INT;\nEND;' Inspection ID: OraMissingBodyInspection", + "markdown": "报告缺少主体声明的软件包和对象类型规范。\n\n声明例程的软件包规范和对象类型以及带有光标的软件包规范必须具有实现这些例程和光标的主体声明。 在程序代码中调用例程或光标时,缺少主体会导致运行时错误。\n\n示例 (Oracle):\n\n CREATE OR REPLACE PACKAGE ppp IS\n FUNCTION foo(a INT) RETURN INT;\n END;\n\nInspection ID: OraMissingBodyInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlMissingBody", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Oracle", + "index": 73, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "OraUnmatchedForwardDeclarationInspection", + "shortDescription": { + "text": "不含定义的前向声明" + }, + "fullDescription": { + "text": "报告在代码中缺少实现的过程和函数的声明。 在 Oracle 中,您可以声明一个没有主体的过程或函数,并在稍后的某个时间编写实现。 该检查将报告未实现的此类过程或函数的名称。 示例 (Oracle): 'DECLARE PROCEDURE foo(a int, b varchar2);\nBEGIN\n NULL;\nEND;' 'foo' 过程已被声明,但缺少实现。 我们可以添加实现来消除错误。 'DECLARE PROCEDURE foo(a int, b varchar2);\n PROCEDURE foo(a int, b varchar2) IS\nBEGIN\n NULL;\nEND;\nBEGIN\n NULL;\nEND;' Inspection ID: OraUnmatchedForwardDeclarationInspection", + "markdown": "报告在代码中缺少实现的过程和函数的声明。\n\n在 Oracle 中,您可以声明一个没有主体的过程或函数,并在稍后的某个时间编写实现。 该检查将报告未实现的此类过程或函数的名称。\n\n示例 (Oracle):\n\n DECLARE PROCEDURE foo(a int, b varchar2);\n BEGIN\n NULL;\n END;\n\n`foo` 过程已被声明,但缺少实现。 我们可以添加实现来消除错误。\n\n DECLARE PROCEDURE foo(a int, b varchar2);\n PROCEDURE foo(a int, b varchar2) IS\n BEGIN\n NULL;\n END;\n BEGIN\n NULL;\n END;\n\nInspection ID: OraUnmatchedForwardDeclarationInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "SqlUnmatchedForwardDeclaration", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Oracle", + "index": 73, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MongoJSExtResolveInspection", + "shortDescription": { + "text": "解析问题" + }, + "fullDescription": { + "text": "报告 MongoDB 和 JavaScript 代码中未解析的引用。 Inspection ID: MongoJSExtResolveInspection", + "markdown": "报告 MongoDB 和 JavaScript 代码中未解析的引用。\n\nInspection ID: MongoJSExtResolveInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MongoJSResolve", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "MongoJS", + "index": 17, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlGotoInspection", + "shortDescription": { + "text": "使用 GOTO 语句" + }, + "fullDescription": { + "text": "报告后向 GOTO 语句和用于退出循环的 GOTO 语句的用法。 通常不推荐大量使用 GOTO 语句。 有关详细信息,请参阅 ibm.com 上的 GOTO statement in SQL procedures。 考虑使用循环而不是使用 GOTO 返回到上一条语句。 考虑使用另一个流控制语句,例如 RETURN 或 BREAK,而不是使用 GOTO 退出 WHILE 循环。 示例 (Oracle): 'CREATE PROCEDURE test(n INT) AS\nDECLARE\n x INT;\nBEGIN\n x := 0;\n GOTO a;\n <
    > x := 1;\n IF (n = 0) THEN\n GOTO a;\n END IF;\n WHILE TRUE\n LOOP\n GOTO b;\n END LOOP;\n <> x := 3;\nEND;' Inspection ID: SqlGotoInspection", + "markdown": "报告后向 GOTO 语句和用于退出循环的 GOTO 语句的用法。\n\n通常不推荐大量使用 GOTO 语句。 有关详细信息,请参阅 [ibm.com 上的 GOTO statement in SQL procedures](https://www.ibm.com/docs/no/db2/11.5?topic=procedures-goto-statement-in-sql)。\n\n考虑使用循环而不是使用 GOTO 返回到上一条语句。\n\n考虑使用另一个流控制语句,例如 RETURN 或 BREAK,而不是使用 GOTO 退出 WHILE 循环。\n\n示例 (Oracle):\n\n CREATE PROCEDURE test(n INT) AS\n DECLARE\n x INT;\n BEGIN\n x := 0;\n GOTO a;\n <> x := 1;\n IF (n = 0) THEN\n GOTO a;\n END IF;\n WHILE TRUE\n LOOP\n GOTO b;\n END LOOP;\n <> x := 3;\n END;\n\nInspection ID: SqlGotoInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlGoto", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlWithoutWhereInspection", + "shortDescription": { + "text": "不带 where 子句的删除或更新语句" + }, + "fullDescription": { + "text": "报告不带 WHERE 子句的 DELETE 或 UPDATE 语句的用法。 如果没有 WHERE 子句,DELETE 会删除表中的所有数据,而 UPDATE 会覆盖所有表行的值。 示例 (MySQL): 'CREATE TABLE t1 (a TEXT, b INT, c BOOLEAN);\nupdate t1 set a = 'Smith';\ndelete from t1;' Inspection ID: SqlWithoutWhereInspection", + "markdown": "报告不带 WHERE 子句的 DELETE 或 UPDATE 语句的用法。\n\n如果没有 WHERE 子句,DELETE 会删除表中的所有数据,而 UPDATE 会覆盖所有表行的值。\n\n示例 (MySQL):\n\n CREATE TABLE t1 (a TEXT, b INT, c BOOLEAN);\n update t1 set a = 'Smith';\n delete from t1;\n\nInspection ID: SqlWithoutWhereInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlWithoutWhere", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlAmbiguousColumnInspection", + "shortDescription": { + "text": "不明确的引用" + }, + "fullDescription": { + "text": "报告名称相同但属于不同表的列。 示例 (MySQL): 'CREATE TABLE foo(id INT PRIMARY KEY);\nCREATE TABLE bar(id INT PRIMARY KEY);\n\nSELECT foo.id, bar.id FROM foo, bar WHERE id > 0;' 'id' 列出现在 'foo' 和 'bar' 表中。 您需要限定列名以使查询正确。 'SELECT foo.id, bar.id FROM foo, bar WHERE foo.id > 0;' Inspection ID: SqlAmbiguousColumnInspection", + "markdown": "报告名称相同但属于不同表的列。\n\n示例 (MySQL):\n\n CREATE TABLE foo(id INT PRIMARY KEY);\n CREATE TABLE bar(id INT PRIMARY KEY);\n\n SELECT foo.id, bar.id FROM foo, bar WHERE id > 0;\n\n`id` 列出现在 `foo` 和 `bar` 表中。 您需要限定列名以使查询正确。\n\n SELECT foo.id, bar.id FROM foo, bar WHERE foo.id > 0;\n\nInspection ID: SqlAmbiguousColumnInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlAmbiguousColumn", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SqlUnusedCteInspection", + "shortDescription": { + "text": "未使用的通用表表达式" + }, + "fullDescription": { + "text": "报告查询中未使用的通用表表达式 (CTE)。 示例 (PostgreSQL): 'CREATE TABLE foo(a INT);\n\nWITH a AS (SELECT 1 AS x FROM foo)\nSELECT 1 + 2 FROM foo;' 通过使用 WITH,我们创建了一个名为 'a' 的临时命名结果集,也称为通用表表达式 (CTE)。 但是,我们稍后不会在代码中使用此 CTE。 未使用的 CTE 显示为灰色。 Inspection ID: SqlUnusedCteInspection", + "markdown": "报告查询中未使用的通用表表达式 (CTE)。\n\n示例 (PostgreSQL):\n\n CREATE TABLE foo(a INT);\n\n WITH a AS (SELECT 1 AS x FROM foo)\n SELECT 1 + 2 FROM foo;\n\n通过使用 WITH,我们创建了一个名为 `a` 的临时命名结果集,也称为通用表表达式 (CTE)。 但是,我们稍后不会在代码中使用此 CTE。 未使用的 CTE 显示为灰色。\n\nInspection ID: SqlUnusedCteInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SqlUnusedCte", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "SQL", + "index": 40, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MongoJSResolveInspection", + "shortDescription": { + "text": "解析问题" + }, + "fullDescription": { + "text": "报告 MongoDB 和 JavaScript 代码中未解析的引用。 示例: 'db\nuse foo\n -- a reference to a non-existing collection\ndb.non_existing_collection\ndb['non_existing_collection']\ndb['non_existing_collection'].find().hasNext()' 'non_existing_collection' 集合在数据库中不存在,将被报告。 Inspection ID: MongoJSResolveInspection", + "markdown": "报告 MongoDB 和 JavaScript 代码中未解析的引用。\n\n示例:\n\n db\n use foo\n -- a reference to a non-existing collection\n db.non_existing_collection\n db['non_existing_collection']\n db['non_existing_collection'].find().hasNext()\n\n`non_existing_collection` 集合在数据库中不存在,将被报告。\n\nInspection ID: MongoJSResolveInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MongoJSResolve", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "MongoJS", + "index": 17, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "org.intellij.plugins.markdown", + "version": "261.26222.73", + "rules": [ + { + "id": "MarkdownOutdatedTableOfContents", + "shortDescription": { + "text": "过期的目录部分" + }, + "fullDescription": { + "text": "检查特定目录部分是否与文档的实际结构相对应。 Inspection ID: MarkdownOutdatedTableOfContents", + "markdown": "检查特定目录部分是否与文档的实际结构相对应。\n\nInspection ID: MarkdownOutdatedTableOfContents" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MarkdownOutdatedTableOfContents", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Markdown", + "index": 23, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MarkdownUnresolvedLinkLabel", + "shortDescription": { + "text": "未解析的链接标签" + }, + "fullDescription": { + "text": "报告 Markdown 文件中的未解析链接标签。 Inspection ID: MarkdownUnresolvedLinkLabel", + "markdown": "报告 Markdown 文件中的未解析链接标签。\n\nInspection ID: MarkdownUnresolvedLinkLabel" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MarkdownUnresolvedLinkLabel", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "Markdown", + "index": 23, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MarkdownIncorrectTableFormatting", + "shortDescription": { + "text": "表格式设置不正确" + }, + "fullDescription": { + "text": "检查表的格式是否正确。 Inspection ID: MarkdownIncorrectTableFormatting", + "markdown": "检查表的格式是否正确。\n\nInspection ID: MarkdownIncorrectTableFormatting" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "MarkdownIncorrectTableFormatting", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "Markdown", + "index": 23, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MarkdownIncorrectlyNumberedListItem", + "shortDescription": { + "text": "列表项编号错误" + }, + "fullDescription": { + "text": "排序列表条目应从 1 开始连续编号。 这背后的动机是,大多数 Markdown 处理器都会忽略有序列表的编号。 处理器将为此类列表生成 '
      ' 元素,该元素将从 1 开始对条目进行连续编号。 Inspection ID: MarkdownIncorrectlyNumberedListItem", + "markdown": "排序列表条目应从 1 开始连续编号。\n\n这背后的动机是,大多数 Markdown 处理器都会忽略有序列表的编号。 处理器将为此类列表生成 `
        ` 元素,该元素将从 1 开始对条目进行连续编号。\n\nInspection ID: MarkdownIncorrectlyNumberedListItem" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MarkdownIncorrectlyNumberedListItem", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "Markdown", + "index": 23, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MarkdownLinkDestinationWithSpaces", + "shortDescription": { + "text": "链接不应包含空格" + }, + "fullDescription": { + "text": "为确保不同工具之间的一致性,文件链接不应包含空格。 示例: '[Some file link](some file.md)' 快速修复会将空格替换为它们的 URL 编码等效项: '[Some file link](some%20file.md)' Inspection ID: MarkdownLinkDestinationWithSpaces", + "markdown": "为确保不同工具之间的一致性,文件链接不应包含空格。\n\n**示例:**\n\n\n [Some file link](some file.md)\n\n快速修复会将空格替换为它们的 URL 编码等效项:\n\n\n [Some file link](some%20file.md)\n\nInspection ID: MarkdownLinkDestinationWithSpaces" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MarkdownLinkDestinationWithSpaces", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "Markdown", + "index": 23, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MarkdownNoTableBorders", + "shortDescription": { + "text": "表没有边框" + }, + "fullDescription": { + "text": "检查表是否有正确的边框。 出于兼容性原因,所有表行的开头和结尾都应该有边框(管道符号)。 Inspection ID: MarkdownNoTableBorders", + "markdown": "检查表是否有正确的边框。 出于兼容性原因,所有表行的开头和结尾都应该有边框(管道符号)。\n\nInspection ID: MarkdownNoTableBorders" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MarkdownNoTableBorders", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "Markdown", + "index": 23, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MarkdownUnresolvedFileReference", + "shortDescription": { + "text": "未解析的文件引用" + }, + "fullDescription": { + "text": "报告 Markdown 文件中的未解析文件引用。 Inspection ID: MarkdownUnresolvedFileReference", + "markdown": "报告 Markdown 文件中的未解析文件引用。\n\nInspection ID: MarkdownUnresolvedFileReference" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MarkdownUnresolvedFileReference", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Markdown", + "index": 23, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MarkdownUnresolvedHeaderReference", + "shortDescription": { + "text": "未解析的头引用" + }, + "fullDescription": { + "text": "报告 Markdown 文件中的未解析头引用。 Inspection ID: MarkdownUnresolvedHeaderReference", + "markdown": "报告 Markdown 文件中的未解析头引用。\n\nInspection ID: MarkdownUnresolvedHeaderReference" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "MarkdownUnresolvedHeaderReference", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "Markdown", + "index": 23, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "com.jetbrains.edu", + "version": "2026.6-2026.1-630", + "rules": [ + { + "id": "StudyItemNotFound", + "shortDescription": { + "text": "找不到任务、课次或小节" + }, + "fullDescription": { + "text": "报告 'course-info.yaml'、'section-info.yaml' 和 'lesson-info.yaml' 文件中与本地文件系统中的任何目录都不对应的路径 Inspection ID: StudyItemNotFound", + "markdown": "报告 `course-info.yaml`、`section-info.yaml` 和 `lesson-info.yaml` 文件中与本地文件系统中的任何目录都不对应的路径\n\nInspection ID: StudyItemNotFound" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "StudyItemNotFound", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "JetBrains Academy/Yaml 配置", + "index": 25, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "AdditionalFileNotFound", + "shortDescription": { + "text": "找不到附加文件" + }, + "fullDescription": { + "text": "报告附加文件列表中 'course-info.yaml' 内指定的路径与本地文件系统中的任何现有文件不对应的情况。 Inspection ID: AdditionalFileNotFound", + "markdown": "报告附加文件列表中 `course-info.yaml` 内指定的路径与本地文件系统中的任何现有文件不对应的情况。\n\nInspection ID: AdditionalFileNotFound" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "AdditionalFileNotFound", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "JetBrains Academy/Yaml 配置", + "index": 25, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "UnsupportedLanguageVersion", + "shortDescription": { + "text": "不支持的编程语言" + }, + "fullDescription": { + "text": "报告课程中不正确的编程语言。 Inspection ID: UnsupportedLanguageVersion", + "markdown": "报告课程中不正确的编程语言。\n\nInspection ID: UnsupportedLanguageVersion" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "UnsupportedLanguageVersion", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "JetBrains Academy/Yaml 配置", + "index": 25, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TaskFileNotFound", + "shortDescription": { + "text": "找不到任务文件" + }, + "fullDescription": { + "text": "报告 'task-info.yaml' 中与本地文件系统中的任何文件都不对应的路径 Inspection ID: TaskFileNotFound", + "markdown": "报告 `task-info.yaml` 中与本地文件系统中的任何文件都不对应的路径\n\nInspection ID: TaskFileNotFound" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "TaskFileNotFound", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "JetBrains Academy/Yaml 配置", + "index": 25, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "DuplicateAdditionalFiles", + "shortDescription": { + "text": "重复的附加文件" + }, + "fullDescription": { + "text": "检测重复的附加文件。 课程归档中的每个附加文件必须具有唯一名称。 Inspection ID: DuplicateAdditionalFiles", + "markdown": "检测重复的附加文件。\n\n\n课程归档中的每个附加文件必须具有唯一名称。\n\nInspection ID: DuplicateAdditionalFiles" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "DuplicateAdditionalFiles", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "JetBrains Academy/Yaml 配置", + "index": 25, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "com.intellij", + "version": "261.26222.73", + "rules": [ + { + "id": "HtmlUnknownBooleanAttribute", + "shortDescription": { + "text": "不正确的布尔特性" + }, + "fullDescription": { + "text": "报告不含值的 HTML 非布尔特性。 建议配置不应报告的特性。 Inspection ID: HtmlUnknownBooleanAttribute", + "markdown": "报告不含值的 HTML 非布尔特性。 建议配置不应报告的特性。\n\nInspection ID: HtmlUnknownBooleanAttribute" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "HtmlUnknownBooleanAttribute", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "DuplicatedCode", + "shortDescription": { + "text": "重复的代码段" + }, + "fullDescription": { + "text": "报告所选作用域内的重复代码块: 相同文件、或整个项目。 检查功能快速修复,可以帮助您设置检测到的重复项的大小,导航至重复的代码段,然后在工具窗口中进行比较。 检查选项允许您选择已报告重复片段的作用域,并为重复语言结构设置初始大小。 您还可以在 文件 | 设置 | 编辑器 | 重复项中配置要匿名化的结构。", + "markdown": "报告所选作用域内的重复代码块: 相同文件、或整个项目。\n\n检查功能快速修复,可以帮助您设置检测到的重复项的大小,导航至重复的代码段,然后在工具窗口中进行比较。\n\n检查选项允许您选择已报告重复片段的作用域,并为重复语言结构设置初始大小。\n\n您还可以在 [文件 \\| 设置 \\| 编辑器 \\| 重复项](settings://duplicates.index)中配置要匿名化的结构。" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "DuplicatedCode", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "常规", + "index": 32, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "InconsistentLineSeparators", + "shortDescription": { + "text": "行分隔符不一致" + }, + "fullDescription": { + "text": "报告包含的行分隔符与项目设置中指定的行分隔符不同的文件。 例如,如果在设置 | 编辑器 | 代码样式 | 行分隔符中将行分隔符设置为 '\\n',而您正在编辑的文件使用 '\\r\\n' 作为行分隔符,就会触发该检查。 该检查还会警告留意文件中的混合行分隔符。 Inspection ID: InconsistentLineSeparators", + "markdown": "报告包含的行分隔符与项目设置中指定的行分隔符不同的文件。\n\n例如,如果在[设置 \\| 编辑器 \\| 代码样式 \\| 行分隔符](settings://preferences.sourceCode?Line%20separator)中将行分隔符设置为 `\\n`,而您正在编辑的文件使用 `\\r\\n` 作为行分隔符,就会触发该检查。\n\n该检查还会警告留意文件中的混合行分隔符。\n\nInspection ID: InconsistentLineSeparators" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "InconsistentLineSeparators", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "常规", + "index": 32, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RedundantSuppression", + "shortDescription": { + "text": "冗余禁止" + }, + "fullDescription": { + "text": "报告由于它们所影响的检查在此上下文中不再适用而可以安全移除的下列元素: '@SuppressWarning' 注解或 '// noinspection' 行注释,或 '/** noinspection */' JavaDoc 注释 示例: 'public class C {\n // 符号已经为 private,\n // 但周围仍有注解\n @SuppressWarnings({\"WeakerAccess\"})\n private boolean CONST = true;\n void f() {\n CONST = false;\n }\n}' Inspection ID: RedundantSuppression", + "markdown": "报告由于它们所影响的检查在此上下文中不再适用而可以安全移除的下列元素:\n\n* `@SuppressWarning` 注解或\n* `// noinspection` 行注释,或\n* `/** noinspection */` JavaDoc 注释\n\n示例:\n\n\n public class C {\n // 符号已经为 private,\n // 但周围仍有注解\n @SuppressWarnings({\"WeakerAccess\"})\n private boolean CONST = true;\n void f() {\n CONST = false;\n }\n }\n\nInspection ID: RedundantSuppression" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "RedundantSuppression", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "常规", + "index": 32, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ProblematicWhitespace", + "shortDescription": { + "text": "有问题的空格" + }, + "fullDescription": { + "text": "报告以下问题: 当代码样式配置为只使用空格时使用制表符进行缩进。 当代码样式配置为只使用制表符时使用空格进行缩进。 当代码样式配置为使用智能制表符时,使用空格进行缩进,以及使用制表符进行对齐。 Inspection ID: ProblematicWhitespace", + "markdown": "报告以下问题:\n\n* 当代码样式配置为只使用空格时使用制表符进行缩进。\n* 当代码样式配置为只使用制表符时使用空格进行缩进。\n* 当代码样式配置为使用智能制表符时,使用空格进行缩进,以及使用制表符进行对齐。\n\n\nInspection ID: ProblematicWhitespace" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ProblematicWhitespace", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "常规", + "index": 32, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlUnknownTarget", + "shortDescription": { + "text": "链接中未解析的文件" + }, + "fullDescription": { + "text": "报告链接中未解析的文件。 Inspection ID: HtmlUnknownTarget", + "markdown": "报告链接中未解析的文件。\n\nInspection ID: HtmlUnknownTarget" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "HtmlUnknownTarget", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "SSBasedInspection", + "shortDescription": { + "text": "结构搜索检查" + }, + "fullDescription": { + "text": "允许配置可以应用于您正在编辑的文件的结构搜索/结构替换模板。 所有匹配项都将高亮显示并使用您配置的模板名称标记。 如果您还配置了结构替换模式,相应的替换选项将作为快速修复提供。 Inspection ID: SSBasedInspection", + "markdown": "允许配置可以应用于您正在编辑的文件的**结构搜索/结构替换**模板。\n\n所有匹配项都将高亮显示并使用您配置的模板名称标记。\n如果您还配置了**结构替换**模式,相应的替换选项将作为快速修复提供。\n\nInspection ID: SSBasedInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "SSBasedInspection", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "结构搜索", + "index": 44, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "LongLine", + "shortDescription": { + "text": "行长度超出代码样式的允许范围" + }, + "fullDescription": { + "text": "报告比在设置 | 编辑器 | 代码样式 | 常规中指定的强制换行位置参数长的行。 Inspection ID: LongLine", + "markdown": "报告比在[设置 \\| 编辑器 \\| 代码样式 \\| 常规](settings://preferences.sourceCode?Hard%20wrap%20at)中指定的**强制换行位置** 参数长的行。\n\nInspection ID: LongLine" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "LongLine", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "常规", + "index": 32, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XmlWrongRootElement", + "shortDescription": { + "text": "错误的根元素" + }, + "fullDescription": { + "text": "报告与 '' 标记中指定的名称不同的根标记名称。 Inspection ID: XmlWrongRootElement", + "markdown": "报告与 `` 标记中指定的名称不同的根标记名称。\n\nInspection ID: XmlWrongRootElement" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "XmlWrongRootElement", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "XML", + "index": 47, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CheckValidXmlInScriptTagBody", + "shortDescription": { + "text": "'script' 标记的内容格式错误" + }, + "fullDescription": { + "text": "报告是无效 XML 的 'script' 标记的内容。 示例: '' 在应用快速修复后: '' Inspection ID: CheckValidXmlInScriptTagBody", + "markdown": "报告是无效 XML 的 `script` 标记的内容。 \n\n**示例:**\n\n\n \n\n在应用快速修复后:\n\n\n \n\nInspection ID: CheckValidXmlInScriptTagBody" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CheckValidXmlInScriptTagBody", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "InjectedReferences", + "shortDescription": { + "text": "已注入的引用" + }, + "fullDescription": { + "text": "报告语言注入所注入的未解析引用。 示例: '@Language(\"file-reference\")\n String fileName = \"/home/user/nonexistent.file\"; // 文件不存在时高亮显示' Inspection ID: InjectedReferences", + "markdown": "报告[语言注入](https://www.jetbrains.com/help/idea/using-language-injections.html)所注入的未解析引用。\n\n示例:\n\n\n @Language(\"file-reference\")\n String fileName = \"/home/user/nonexistent.file\"; // 文件不存在时高亮显示\n\nInspection ID: InjectedReferences" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "InjectedReferences", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "常规", + "index": 32, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RegExpSuspiciousBackref", + "shortDescription": { + "text": "可疑的反向引用" + }, + "fullDescription": { + "text": "报告在运行时无法解析的反向引用。 这意味着反向引用永远无法匹配任何内容。 如果组是在反向引用之后定义的,或者组是在替代项的不同分支中定义的,则反向引用将不可解析。 在其反向引用之后定义的组的示例: '\\1(abc)' 不同分支中的组和反向引用的示例: 'a(b)c|(xy)\\1z' 2022.1 最新变化 Inspection ID: RegExpSuspiciousBackref", + "markdown": "报告在运行时无法解析的反向引用。 这意味着反向引用永远无法匹配任何内容。 如果组是在反向引用之后定义的,或者组是在替代项的不同分支中定义的,则反向引用将不可解析。\n\n**在其反向引用之后定义的组的示例:**\n\n\n \\1(abc)\n\n**不同分支中的组和反向引用的示例:**\n\n\n a(b)c|(xy)\\1z\n\n2022.1 最新变化\n\nInspection ID: RegExpSuspiciousBackref" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "RegExpSuspiciousBackref", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RegExpSingleCharAlternation", + "shortDescription": { + "text": "单字符替代项" + }, + "fullDescription": { + "text": "报告正则表达式中的单一字符替换。 改用一个字符类更简单。 这还可能提高匹配性能。 示例: 'a|b|c|d' 在应用快速修复后: '[abcd]' 2017.1 最新变化 Inspection ID: RegExpSingleCharAlternation", + "markdown": "报告正则表达式中的单一字符替换。 改用一个字符类更简单。 这还可能提高匹配性能。\n\n**示例:**\n\n\n a|b|c|d\n\n在应用快速修复后:\n\n\n [abcd]\n\n\n2017.1 最新变化\n\nInspection ID: RegExpSingleCharAlternation" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "RegExpSingleCharAlternation", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Performance" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlUnknownAttribute", + "shortDescription": { + "text": "未知特性" + }, + "fullDescription": { + "text": "报告未知的 HTML 特性。 建议配置不应报告的特性。 Inspection ID: HtmlUnknownAttribute", + "markdown": "报告未知的 HTML 特性。 建议配置不应报告的特性。\n\nInspection ID: HtmlUnknownAttribute" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "HtmlUnknownAttribute", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CheckTagEmptyBody", + "shortDescription": { + "text": "空元素内容" + }, + "fullDescription": { + "text": "报告没有内容的 XML 元素。 示例: '\n \n ' 在应用快速修复后: '\n \n ' Inspection ID: CheckTagEmptyBody", + "markdown": "报告没有内容的 XML 元素。\n\n**示例:**\n\n\n \n \n \n\n在应用快速修复后:\n\n\n \n \n \n\nInspection ID: CheckTagEmptyBody" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CheckTagEmptyBody", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "XML", + "index": 47, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RegExpRedundantEscape", + "shortDescription": { + "text": "冗余字符转义" + }, + "fullDescription": { + "text": "报告可以替换为保留其含义的非转义字符的冗余字符转义序列。 许多在字符类外部必需的转义序列在字符类的方括号 '[]' 内部冗余。 尽管某些方言(JavaScript、Python 等)允许在字符类外部使用未转义的左大括号 '{',但这样可能会导致混乱并降低模式的可移植性,因为某些方言要求将大括号作为字符进行转义。 因此,该检查不会报告转义的左大括号。 示例: '\\-\\;[\\.]' 在应用快速修复后: '-;[.]' 忽略转义的右括号 '}' 和 ']' 选项可以指定当 RegExp 方言允许在字符类外部使用未转义的 '\\}' 和 '\\]' 时是否报告它们。 同样,忽略转义斜杠 '/' 选项指定是否在 RegExp 方言未转义斜杠时报告 '\\/'。 2017.3 最新变化 Inspection ID: RegExpRedundantEscape", + "markdown": "报告可以替换为保留其含义的非转义字符的冗余字符转义序列。 许多在字符类外部必需的转义序列在字符类的方括号 `[]` 内部冗余。\n\n\n尽管某些方言(JavaScript、Python 等)允许在字符类外部使用未转义的左大括号 `{`,但这样可能会导致混乱并降低模式的可移植性,因为某些方言要求将大括号作为字符进行转义。\n因此,该检查不会报告转义的左大括号。\n\n**示例:**\n\n\n \\-\\;[\\.]\n\n在应用快速修复后:\n\n\n -;[.]\n\n\n**忽略转义的右括号 '}' 和 '\\]'** 选项可以指定当 RegExp 方言允许在字符类外部使用未转义的 `\\}` 和 `\\]` 时是否报告它们。\n\n\n同样,**忽略转义斜杠 '/'** 选项指定是否在 RegExp 方言未转义斜杠时报告 `\\/`。\n\n2017.3 最新变化\n\nInspection ID: RegExpRedundantEscape" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "RegExpRedundantEscape", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "UnresolvedReference", + "shortDescription": { + "text": "未解析的引用" + }, + "fullDescription": { + "text": "报告使用 XML 语法的 RELAX-NG 文件中对命名模式 ('define') 的未解析引用。 建议创建引用的 'define' 元素。 Inspection ID: UnresolvedReference", + "markdown": "报告使用 XML 语法的 RELAX-NG 文件中对命名模式 (`define`) 的未解析引用。 建议创建引用的 `define` 元素。\n\nInspection ID: UnresolvedReference" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "UnresolvedReference", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "RELAX NG", + "index": 56, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlMissingClosingTag", + "shortDescription": { + "text": "缺少结束标记" + }, + "fullDescription": { + "text": "报告不含结束标记的 HTML 元素。 即便在可选的情况下,有些编码样式也要求 HTML 元素包含结束标记。 示例: '\n \n

        Behold!\n \n ' 在应用快速修复后: '\n \n

        Behold!

        \n \n ' Inspection ID: HtmlMissingClosingTag", + "markdown": "报告不含结束标记的 HTML 元素。 即便在可选的情况下,有些编码样式也要求 HTML 元素包含结束标记。\n\n**示例:**\n\n\n \n \n

        Behold!\n \n \n\n在应用快速修复后:\n\n\n \n \n

        Behold!

        \n \n \n\nInspection ID: HtmlMissingClosingTag" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "HtmlMissingClosingTag", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CustomRegExpInspection", + "shortDescription": { + "text": "自定义正则表达式检查" + }, + "fullDescription": { + "text": "自定义正则表达式检查 Inspection ID: CustomRegExpInspection", + "markdown": "自定义正则表达式检查\n\nInspection ID: CustomRegExpInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CustomRegExpInspection", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "IncorrectFormatting", + "shortDescription": { + "text": "格式设置不正确" + }, + "fullDescription": { + "text": "报告代码不遵循项目代码样式设置时出现的格式问题。 此检查与需要第三方格式化程序进行代码格式设置的语言(例如启用了 CLangFormat 的 Go 或 C 语言)不兼容。 Inspection ID: IncorrectFormatting", + "markdown": "报告代码不遵循项目代码样式设置时出现的格式问题。\n\n\n此检查与需要第三方格式化程序进行代码格式设置的语言(例如启用了 CLangFormat 的 Go 或 C 语言)不兼容。\n\nInspection ID: IncorrectFormatting" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "IncorrectFormatting", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "常规", + "index": 32, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlExtraClosingTag", + "shortDescription": { + "text": "冗余结束标记" + }, + "fullDescription": { + "text": "报告空元素的冗余结束标记,例如 'img' 或 'br'。 示例: '\n \n

        \n \n ' 在应用快速修复后: '\n \n
        \n \n ' Inspection ID: HtmlExtraClosingTag", + "markdown": "报告空元素的冗余结束标记,例如 `img` 或 `br`。\n\n**示例:**\n\n\n \n \n

        \n \n \n\n在应用快速修复后:\n\n\n \n \n
        \n \n \n\nInspection ID: HtmlExtraClosingTag" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "HtmlExtraClosingTag", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlUnknownAnchorTarget", + "shortDescription": { + "text": "链接中未解析的片段" + }, + "fullDescription": { + "text": "报告 '#' 符号后面的 URL 中未解析的最后部分。 Inspection ID: HtmlUnknownAnchorTarget", + "markdown": "报告 `#` 符号后面的 URL 中未解析的最后部分。\n\nInspection ID: HtmlUnknownAnchorTarget" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "HtmlUnknownAnchorTarget", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RegExpUnexpectedAnchor", + "shortDescription": { + "text": "起始或结束定位点在意外位置" + }, + "fullDescription": { + "text": "报告不在模式开头的 '^' 或 '\\A' 定位标记以及不在模式末尾的 '$'、'\\Z' 或 '\\z' 定位标记。 如果这些正则表达式定位标记位于错误的位置,则会阻止模式与任何对象匹配。 对于 '^' 和 '$' 定位标记,则很可能是指字面量字符,并且忘记了转义。 示例: '(Price $10)' 2018.1 最新变化 Inspection ID: RegExpUnexpectedAnchor", + "markdown": "报告不在模式开头的 `^` 或 `\\A` 定位标记以及不在模式末尾的 `$`、`\\Z` 或 `\\z` 定位标记。 如果这些正则表达式定位标记位于错误的位置,则会阻止模式与任何对象匹配。 对于 `^` 和 `$` 定位标记,则很可能是指字面量字符,并且忘记了转义。\n\n**示例:**\n\n\n (Price $10)\n\n\n2018.1 最新变化\n\nInspection ID: RegExpUnexpectedAnchor" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "RegExpUnexpectedAnchor", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CheckXmlFileWithXercesValidator", + "shortDescription": { + "text": "外部验证失败" + }, + "fullDescription": { + "text": "报告 Xerces 验证程序检测到的 XML 文件和指定 DTD 或架构的差异。 Inspection ID: CheckXmlFileWithXercesValidator", + "markdown": "报告 Xerces 验证程序检测到的 XML 文件和指定 DTD 或架构的差异。\n\nInspection ID: CheckXmlFileWithXercesValidator" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CheckXmlFileWithXercesValidator", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "XML", + "index": 47, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlUnknownTag", + "shortDescription": { + "text": "未知标记" + }, + "fullDescription": { + "text": "报告未知的 HTML 标记。 建议配置不应报告的标记。 Inspection ID: HtmlUnknownTag", + "markdown": "报告未知的 HTML 标记。 建议配置不应报告的标记。\n\nInspection ID: HtmlUnknownTag" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "HtmlUnknownTag", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RegExpEscapedMetaCharacter", + "shortDescription": { + "text": "转义元字符" + }, + "fullDescription": { + "text": "报告转义元字符。 某些正则表达式代码样式指定应将元字符放在字符类中,从而使正则表达式更易于理解。 此检查不会警告元字符 '[', ']' 和 '^',因为这些字符可能需要在字符类中进行额外的转义。 示例: '\\d+\\.\\d+' 在应用快速修复后: '\\d+[.]\\d+' 2017.1 最新变化 Inspection ID: RegExpEscapedMetaCharacter", + "markdown": "报告转义元字符。 某些正则表达式代码样式指定应将元字符放在字符类中,从而使正则表达式更易于理解。 此检查不会警告元字符 `[`, `]` 和 `^`,因为这些字符可能需要在字符类中进行额外的转义。\n\n**示例:**\n\n\n \\d+\\.\\d+\n\n在应用快速修复后:\n\n\n \\d+[.]\\d+\n\n2017.1 最新变化\n\nInspection ID: RegExpEscapedMetaCharacter" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "RegExpEscapedMetaCharacter", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XmlHighlighting", + "shortDescription": { + "text": "XML 高亮显示" + }, + "fullDescription": { + "text": "报告批量代码检查结果中的 XML 验证问题。 Inspection ID: XmlHighlighting", + "markdown": "报告批量代码检查结果中的 XML 验证问题。\n\nInspection ID: XmlHighlighting" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "XmlHighlighting", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "XML", + "index": 47, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XmlDuplicatedId", + "shortDescription": { + "text": "重复 'id' 特性" + }, + "fullDescription": { + "text": "报告 XML 和 HTML 中 'id' 特性的重复值。 Inspection ID: XmlDuplicatedId", + "markdown": "报告 XML 和 HTML 中 `id` 特性的重复值。\n\nInspection ID: XmlDuplicatedId" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "XmlDuplicatedId", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "XML", + "index": 47, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RegExpDuplicateCharacterInClass", + "shortDescription": { + "text": "字符类中存在重复字符" + }, + "fullDescription": { + "text": "报告正则表达式字符类中的重复字符。 重复字符是多余的,可将其移除而不改变正则表达式的语义。 示例: '[aabc]' 在应用快速修复后: '[abc]' Inspection ID: RegExpDuplicateCharacterInClass", + "markdown": "报告正则表达式字符类中的重复字符。 重复字符是多余的,可将其移除而不改变正则表达式的语义。\n\n**示例:**\n\n\n [aabc]\n\n在应用快速修复后:\n\n\n [abc]\n\nInspection ID: RegExpDuplicateCharacterInClass" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "RegExpDuplicateCharacterInClass", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XmlInvalidId", + "shortDescription": { + "text": "未解析的 'id' 引用" + }, + "fullDescription": { + "text": "报告未在 XML 和 HTML 中的任何地方定义的 'id' 的使用情况。 Inspection ID: XmlInvalidId", + "markdown": "报告未在 XML 和 HTML 中的任何地方定义的 `id` 的使用情况。\n\nInspection ID: XmlInvalidId" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "XmlInvalidId", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "XML", + "index": 47, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XmlUnboundNsPrefix", + "shortDescription": { + "text": "未绑定的命名空间前缀" + }, + "fullDescription": { + "text": "报告 XML 中未绑定的命名空间前缀。 Inspection ID: XmlUnboundNsPrefix", + "markdown": "报告 XML 中未绑定的命名空间前缀。\n\nInspection ID: XmlUnboundNsPrefix" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "XmlUnboundNsPrefix", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "XML", + "index": 47, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RequiredAttributes", + "shortDescription": { + "text": "缺少必要的特性" + }, + "fullDescription": { + "text": "报告 XML/HTML 标记中缺少的强制特性。 建议配置不应报告的特性。 Inspection ID: RequiredAttributes", + "markdown": "报告 XML/HTML 标记中缺少的强制特性。 建议配置不应报告的特性。\n\nInspection ID: RequiredAttributes" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "RequiredAttributes", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "ReassignedToPlainText", + "shortDescription": { + "text": "重新分配为纯文本" + }, + "fullDescription": { + "text": "报告被显式重新分配为纯文本文件类型的文件。 这种关联是不必要的,因为平台会根据内容自动检测文本文件。 您可以通过在设置 | 编辑器 | 文件类型 | 文本中移除文件类型关联来关闭此警告。 Inspection ID: ReassignedToPlainText", + "markdown": "报告被显式重新分配为纯文本文件类型的文件。 这种关联是不必要的,因为平台会根据内容自动检测文本文件。\n\n您可以通过在**设置 \\| 编辑器 \\| 文件类型 \\| 文本**中移除文件类型关联来关闭此警告。\n\nInspection ID: ReassignedToPlainText" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "ReassignedToPlainText", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "常规", + "index": 32, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XmlUnusedNamespaceDeclaration", + "shortDescription": { + "text": "未使用的架构声明" + }, + "fullDescription": { + "text": "报告 XML 中未使用的命名空间声明或位置提示。 Inspection ID: XmlUnusedNamespaceDeclaration", + "markdown": "报告 XML 中未使用的命名空间声明或位置提示。\n\nInspection ID: XmlUnusedNamespaceDeclaration" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "XmlUnusedNamespaceDeclaration", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "XML", + "index": 47, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RegExpRedundantClassElement", + "shortDescription": { + "text": "冗余的 '\\d', '[:digit:]',或 '\\D' 类元素" + }, + "fullDescription": { + "text": "报告在同一个类中与 '\\w' 或 '[:word:]'(带 '\\W' 的 '\\D')一起使用且可移除的冗余 '\\d' 或 '[:digit:]'。 示例: '[\\w\\d]' 在应用快速修复后: '[\\w]' 2022.2 最新变化 Inspection ID: RegExpRedundantClassElement", + "markdown": "报告在同一个类中与 `\\w` 或 `[:word:]`(带 `\\W` 的 `\\D`)一起使用且可移除的冗余 `\\d` 或 `[:digit:]`。\n\n**示例:**\n\n\n [\\w\\d]\n\n在应用快速修复后:\n\n\n [\\w]\n\n2022.2 最新变化\n\nInspection ID: RegExpRedundantClassElement" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "RegExpRedundantClassElement", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RegExpSimplifiable", + "shortDescription": { + "text": "正则表达式可以简化" + }, + "fullDescription": { + "text": "报告可以简化的正则表达式。 示例: '[a] xx* [ah-hz]' 在应用快速修复后: 'a x+ [ahz]' 2022.1 最新变化 Inspection ID: RegExpSimplifiable", + "markdown": "报告可以简化的正则表达式。\n\n**示例:**\n\n\n [a] xx* [ah-hz]\n\n在应用快速修复后:\n\n\n a x+ [ahz]\n\n2022.1 最新变化\n\nInspection ID: RegExpSimplifiable" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "RegExpSimplifiable", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RegExpEmptyAlternationBranch", + "shortDescription": { + "text": "替代项中存在空分支" + }, + "fullDescription": { + "text": "报告正则表达式替换中的空分支。 空分支只能匹配空字符串,在大多数情况下,这并不是我们的目的。 此检查不会报告位于替换开头或末尾的单个空分支。 示例: '(alpha||bravo)' 在应用快速修复后: '(alpha|bravo)' 2017.2 最新变化 Inspection ID: RegExpEmptyAlternationBranch", + "markdown": "报告正则表达式替换中的空分支。 空分支只能匹配空字符串,在大多数情况下,这并不是我们的目的。 此检查不会报告位于替换开头或末尾的单个空分支。\n\n**示例:**\n\n\n (alpha||bravo)\n\n在应用快速修复后:\n\n\n (alpha|bravo)\n\n2017.2 最新变化\n\nInspection ID: RegExpEmptyAlternationBranch" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "RegExpEmptyAlternationBranch", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "Annotator", + "shortDescription": { + "text": "注解器" + }, + "fullDescription": { + "text": "报告批处理代码检查运行结果中对此文件至关重要的问题(例如,语法错误)。 与检查不同,这些问题通常会在编辑器中高亮显示,并且无法配置。 这些选项控制此检查所执行检查的作用域: 选项报告语法错误:报告解析器相关问题。 选项报告来自语言特定注解器的问题:报告由为相关语言配置的注解器发现的问题 请参阅自定义语言支持:注解器以了解详情。 选项报告其他高亮显示问题:报告特定于当前文件语言的问题(例如,类型不匹配或未报告的异常)。 请参阅自定义语言支持:高亮显示以了解详情。 Inspection ID: Annotator", + "markdown": "报告批处理代码检查运行结果中对此文件至关重要的问题(例如,语法错误)。 与检查不同,这些问题通常会在编辑器中高亮显示,并且无法配置。 这些选项控制此检查所执行检查的作用域:\n\n* 选项**报告语法错误**:报告解析器相关问题。\n* 选项**报告来自语言特定注解器的问题** :报告由为相关语言配置的注解器发现的问题 请参阅[自定义语言支持:注解器](https://plugins.jetbrains.com/docs/intellij/annotator.html)以了解详情。\n* 选项**报告其他高亮显示问题** :报告特定于当前文件语言的问题(例如,类型不匹配或未报告的异常)。 请参阅[自定义语言支持:高亮显示](https://plugins.jetbrains.com/docs/intellij/syntax-highlighting-and-error-highlighting.html#semantic-highlighting)以了解详情。\n\nInspection ID: Annotator" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "Annotator", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "常规", + "index": 32, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XmlPathReference", + "shortDescription": { + "text": "未解析的文件引用" + }, + "fullDescription": { + "text": "报告 XML 中未解析的文件引用。 Inspection ID: XmlPathReference", + "markdown": "报告 XML 中未解析的文件引用。\n\nInspection ID: XmlPathReference" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "XmlPathReference", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "XML", + "index": 47, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RegExpUnnecessaryNonCapturingGroup", + "shortDescription": { + "text": "不必要的非捕获组" + }, + "fullDescription": { + "text": "报告不必要的非捕获组(对匹配结果无影响)。 示例: 'Everybody be cool, (?:this) is a robbery!' 在应用快速修复后: 'Everybody be cool, this is a robbery!' 2021.1 最新变化 Inspection ID: RegExpUnnecessaryNonCapturingGroup", + "markdown": "报告不必要的非捕获组(对匹配结果无影响)。\n\n**示例:**\n\n\n Everybody be cool, (?:this) is a robbery!\n\n在应用快速修复后:\n\n\n Everybody be cool, this is a robbery!\n\n2021.1 最新变化\n\nInspection ID: RegExpUnnecessaryNonCapturingGroup" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "RegExpUnnecessaryNonCapturingGroup", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "TodoComment", + "shortDescription": { + "text": "TODO 注释" + }, + "fullDescription": { + "text": "报告代码中的 TODO 注释。 您可以在设置 | 编辑器 | TODO 中配置 TODO 注释的格式。 启用只对没有任何详细信息的 TODO 注释发出警告选项,以仅对不提供有关应完成任务的任何描述的空 TODO 注释发出警告。 禁用可报告所有 TODO 注释。 Inspection ID: TodoComment", + "markdown": "报告代码中的 **TODO** 注释。\n\n您可以在[设置 \\| 编辑器 \\| TODO](settings://preferences.toDoOptions) 中配置 **TODO** 注释的格式。\n\n启用**只对没有任何详细信息的 TODO 注释发出警告**选项,以仅对不提供有关应完成任务的任何描述的空 TODO 注释发出警告。 禁用可报告所有 TODO 注释。\n\nInspection ID: TodoComment" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "TodoComment", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "常规", + "index": 32, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RegExpRedundantNestedCharacterClass", + "shortDescription": { + "text": "冗余嵌套字符类" + }, + "fullDescription": { + "text": "报告非必要的嵌套字符类。 示例: '[a-c[x-z]]' 在应用快速修复后: '[a-cx-z]' 2020.2 最新变化 Inspection ID: RegExpRedundantNestedCharacterClass", + "markdown": "报告非必要的嵌套字符类。\n\n**示例:**\n\n\n [a-c[x-z]]\n\n在应用快速修复后:\n\n\n [a-cx-z]\n\n2020.2 最新变化\n\nInspection ID: RegExpRedundantNestedCharacterClass" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "RegExpRedundantNestedCharacterClass", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XmlDeprecatedElement", + "shortDescription": { + "text": "弃用的符号" + }, + "fullDescription": { + "text": "报告弃用的 XML 元素或特性。 可以通过 XML 注释或带有 'deprecated' 文本的文档标记来标记符号。 Inspection ID: XmlDeprecatedElement", + "markdown": "报告弃用的 XML 元素或特性。\n\n可以通过 XML 注释或带有 'deprecated' 文本的文档标记来标记符号。\n\nInspection ID: XmlDeprecatedElement" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "XmlDeprecatedElement", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "XML", + "index": 47, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlWrongAttributeValue", + "shortDescription": { + "text": "特性值错误" + }, + "fullDescription": { + "text": "报告不正确的 HTML 特性值。 Inspection ID: HtmlWrongAttributeValue", + "markdown": "报告不正确的 HTML 特性值。\n\nInspection ID: HtmlWrongAttributeValue" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "HtmlWrongAttributeValue", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XmlDefaultAttributeValue", + "shortDescription": { + "text": "包含默认值的冗余特性" + }, + "fullDescription": { + "text": "报告对 XML 特性默认值的冗余赋值。 Inspection ID: XmlDefaultAttributeValue", + "markdown": "报告对 XML 特性默认值的冗余赋值。\n\nInspection ID: XmlDefaultAttributeValue" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "XmlDefaultAttributeValue", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "XML", + "index": 47, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RegExpOctalEscape", + "shortDescription": { + "text": "八进制转义" + }, + "fullDescription": { + "text": "报告八进制转义(容易与反向引用混淆)。 使用十六进制转义可避免混淆。 示例: '\\07' 在应用快速修复后: '\\x07' 2017.1 最新变化 Inspection ID: RegExpOctalEscape", + "markdown": "报告八进制转义(容易与反向引用混淆)。 使用十六进制转义可避免混淆。\n\n**示例:**\n\n\n \\07\n\n在应用快速修复后:\n\n\n \\x07\n\n2017.1 最新变化\n\nInspection ID: RegExpOctalEscape" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "RegExpOctalEscape", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "UnusedDefine", + "shortDescription": { + "text": "未使用的定义" + }, + "fullDescription": { + "text": "报告 RELAX-NG 文件 (XML 或 Compact 语法) 中未使用的命名模式 ('define')。 通过另一个文件中的 include 使用的 'define' 元素将被忽略。 Inspection ID: UnusedDefine", + "markdown": "报告 RELAX-NG 文件 (XML 或 Compact 语法) 中未使用的命名模式 (`define`)。 通过另一个文件中的 include 使用的 `define` 元素将被忽略。\n\nInspection ID: UnusedDefine" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "UnusedDefine", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "RELAX NG", + "index": 56, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "EmptyDirectory", + "shortDescription": { + "text": "空目录" + }, + "fullDescription": { + "text": "报告空目录。 仅适用于代码 | 检查代码或代码 | 分析代码 | 通过名称运行检查,并且不会在编辑器中报告。 使用仅报告位于源文件夹下的空目录选项可以仅报告源根下的目录。 Inspection ID: EmptyDirectory", + "markdown": "报告空目录。\n\n仅适用于**代码 \\| 检查代码** 或**代码 \\| 分析代码 \\| 通过名称运行检查**,并且不会在编辑器中报告。\n\n使用**仅报告位于源文件夹下的空目录**选项可以仅报告源根下的目录。\n\n\nInspection ID: EmptyDirectory" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "EmptyDirectory", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "常规", + "index": 32, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RegExpAnonymousGroup", + "shortDescription": { + "text": "匿名捕获组或数字反向引用" + }, + "fullDescription": { + "text": "报告正则表达式中的匿名捕获组和数字反向引用。 只有正则表达式方言支持命名组和命名组引用,才支持这些用法。 命名组和命名反向引用可改进代码可读性,建议改用。 无需捕获时,使用非捕获组,即使用 '(?:xxx)' 而不是 '(xxx)',可以提高匹配效率和减少需要使用的内存。 示例: '(\\d\\d\\d\\d)\\1' 更好的正则表达式模式如下: '(?\\d\\d\\d\\d)\\k' 2017.2 最新变化 Inspection ID: RegExpAnonymousGroup", + "markdown": "报告正则表达式中的匿名捕获组和数字反向引用。 只有正则表达式方言支持命名组和命名组引用,才支持这些用法。 命名组和命名反向引用可改进代码可读性,建议改用。 无需捕获时,使用非捕获组,即使用 `(?:xxx)` 而不是 `(xxx)`,可以提高匹配效率和减少需要使用的内存。\n\n**示例:**\n\n\n (\\d\\d\\d\\d)\\1\n\n更好的正则表达式模式如下:\n\n\n (?\\d\\d\\d\\d)\\k\n\n2017.2 最新变化\n\nInspection ID: RegExpAnonymousGroup" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "RegExpAnonymousGroup", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CheckDtdRefs", + "shortDescription": { + "text": "未解析的 DTD 引用" + }, + "fullDescription": { + "text": "报告特定于 DTD 的引用(例如,对 XML 实体或 DTD 元素声明的引用)中的不一致。 适用于 DTD 和 XML 文件。 Inspection ID: CheckDtdRefs", + "markdown": "报告特定于 DTD 的引用(例如,对 XML 实体或 DTD 元素声明的引用)中的不一致。 适用于 DTD 和 XML 文件。\n\nInspection ID: CheckDtdRefs" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CheckDtdRefs", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "XML", + "index": 47, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "NonAsciiCharacters", + "shortDescription": { + "text": "非 ASCII 字符" + }, + "fullDescription": { + "text": "报告在异常上下文中使用非 ASCII 符号的代码元素。 示例: 标识符、字符串或注释中使用的非 ASCII 字符。 使用不同语言编写的标识符,例如带有使用西里尔文编写的字母 'C' 的 'myСollection'。 包含 Unicode 符号(如长划线和箭头)的注释或字符串。 Inspection ID: NonAsciiCharacters", + "markdown": "报告在异常上下文中使用非 ASCII 符号的代码元素。\n\n示例:\n\n* 标识符、字符串或注释中使用的非 ASCII 字符。\n* 使用不同语言编写的标识符,例如带有使用西里尔文编写的字母 **C** 的 `my`**С**`ollection`。\n* 包含 Unicode 符号(如长划线和箭头)的注释或字符串。\n\nInspection ID: NonAsciiCharacters" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "NonAsciiCharacters", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "国际化", + "index": 74, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "XmlUnresolvedReference", + "shortDescription": { + "text": "未解析的引用" + }, + "fullDescription": { + "text": "报告 XML 中的未解析引用。 Inspection ID: XmlUnresolvedReference", + "markdown": "报告 XML 中的未解析引用。\n\nInspection ID: XmlUnresolvedReference" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "XmlUnresolvedReference", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "XML", + "index": 47, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "LossyEncoding", + "shortDescription": { + "text": "有损编码" + }, + "fullDescription": { + "text": "报告由于当前文档编码而无法显示的字符。 示例: 如果在带有 US-ASCII 字符集的文档中键入国际字符,保存时会丢失某些字符。 如果加载使用 ISO-8859-1 一字节字符集的 UTF-8 编码文件,某些字符将无法正确显示。 要解决此问题,可以直接在文件中指定编码来更改文件编码,例如编辑 XML 文件的 XML prolog 中的 'encoding=' 特性,或者在设置 | 编辑器 | 文件编码中更改相应选项。 Inspection ID: LossyEncoding", + "markdown": "报告由于当前文档编码而无法显示的字符。\n\n示例:\n\n* 如果在带有 **US-ASCII** 字符集的文档中键入国际字符,保存时会丢失某些字符。\n* 如果加载使用 **ISO-8859-1** 一字节字符集的 **UTF-8** 编码文件,某些字符将无法正确显示。\n\n要解决此问题,可以直接在文件中指定编码来更改文件编码,例如编辑 XML 文件的 XML prolog 中的 `encoding=` 特性,或者在**设置 \\| 编辑器 \\| 文件编码**中更改相应选项。\n\nInspection ID: LossyEncoding" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "LossyEncoding", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "国际化", + "index": 74, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RegExpRepeatedSpace", + "shortDescription": { + "text": "连续空格" + }, + "fullDescription": { + "text": "报告正则表达式中的多个连续空格。 由于空格默认不可见,因此,很难了解需要的空格数量。 使用单个空格和计数量词来替换连续空格可以让正则表达式更清晰易懂。 示例: '( )' 在应用快速修复后: '( {5})' 2017.1 最新变化 Inspection ID: RegExpRepeatedSpace", + "markdown": "报告正则表达式中的多个连续空格。 由于空格默认不可见,因此,很难了解需要的空格数量。 使用单个空格和计数量词来替换连续空格可以让正则表达式更清晰易懂。\n\n**示例:**\n\n\n ( )\n\n在应用快速修复后:\n\n\n ( {5})\n\n\n2017.1 最新变化\n\nInspection ID: RegExpRepeatedSpace" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "RegExpRepeatedSpace", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "RegExpDuplicateAlternationBranch", + "shortDescription": { + "text": "替代项中存在重复分支" + }, + "fullDescription": { + "text": "报告正则表达式替换中的重复分支。 重复分支会降低匹配速度并导致表达式意图不明。 示例: '(alpha|bravo|charlie|alpha)' 在应用快速修复后: '(alpha|bravo|charlie)' 2017.1 最新变化 Inspection ID: RegExpDuplicateAlternationBranch", + "markdown": "报告正则表达式替换中的重复分支。 重复分支会降低匹配速度并导致表达式意图不明。\n\n**示例:**\n\n\n (alpha|bravo|charlie|alpha)\n\n在应用快速修复后:\n\n\n (alpha|bravo|charlie)\n\n2017.1 最新变化\n\nInspection ID: RegExpDuplicateAlternationBranch" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "RegExpDuplicateAlternationBranch", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Performance" + } + }, + "relationships": [ + { + "target": { + "id": "正则表达式", + "index": 51, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "IgnoreFileDuplicateEntry", + "shortDescription": { + "text": "忽略文件重复项" + }, + "fullDescription": { + "text": "报告忽略文件(例如 .gitignore、.hgignore)中的重复条目(模式)。 这些文件中的重复条目冗余,可以移除。 示例: '# 输出目录\n /out/\n /target/\n /out/' Inspection ID: IgnoreFileDuplicateEntry", + "markdown": "报告忽略文件(例如 .gitignore、.hgignore)中的重复条目(模式)。 这些文件中的重复条目冗余,可以移除。\n\n示例:\n\n\n # 输出目录\n /out/\n /target/\n /out/\n\nInspection ID: IgnoreFileDuplicateEntry" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "IgnoreFileDuplicateEntry", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "版本控制", + "index": 76, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CheckEmptyScriptTag", + "shortDescription": { + "text": "空标记" + }, + "fullDescription": { + "text": "报告在某些浏览器中无法正常运行的空标记。 示例: '\n \n ' Inspection ID: CheckEmptyScriptTag", + "markdown": "报告在某些浏览器中无法正常运行的空标记。\n\n**示例:**\n\n\n \n \n \n\nInspection ID: CheckEmptyScriptTag" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CheckEmptyScriptTag", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "HtmlTools", + "version": "261.26222.73", + "rules": [ + { + "id": "HtmlRequiredAltAttribute", + "shortDescription": { + "text": "缺少所需的 'alt' 特性" + }, + "fullDescription": { + "text": "报告 'img' 或 'applet' 标记或图像映射的 'area' 元素中缺少的 'alt' 特性。 建议添加含标签内容替代文本的必要特性。 基于 WCAG 2.0:H24、H35、H36、H37。 Inspection ID: HtmlRequiredAltAttribute", + "markdown": "报告 `img` 或 `applet` 标记或图像映射的 `area` 元素中缺少的 `alt` 特性。 建议添加含标签内容替代文本的必要特性。 基于 WCAG 2.0:[H24](https://www.w3.org/TR/WCAG20-TECHS/H24.html)、[H35](https://www.w3.org/TR/WCAG20-TECHS/H35.html)、[H36](https://www.w3.org/TR/WCAG20-TECHS/H36.html)、[H37](https://www.w3.org/TR/WCAG20-TECHS/H37.html)。\n\nInspection ID: HtmlRequiredAltAttribute" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "HtmlRequiredAltAttribute", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "HTML/可访问性", + "index": 33, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlFormInputWithoutLabel", + "shortDescription": { + "text": "表单输入没有相关的label" + }, + "fullDescription": { + "text": "报告没有关联标签的表单元素 ('input'、'textarea' 或 'select')。 建议创建新标签。 基于 WCAG 2.0:H44。 Inspection ID: HtmlFormInputWithoutLabel", + "markdown": "报告没有关联标签的表单元素 (`input`、`textarea` 或 `select`)。 建议创建新标签。 基于 WCAG 2.0:[H44](https://www.w3.org/TR/WCAG20-TECHS/H44.html)。 \n\nInspection ID: HtmlFormInputWithoutLabel" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "HtmlFormInputWithoutLabel", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "HTML/可访问性", + "index": 33, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlRequiredTitleAttribute", + "shortDescription": { + "text": "缺少所需的 'title' 特性" + }, + "fullDescription": { + "text": "报告缺少的标题特性 'frame'、'iframe'、'dl' 和 'a' 标记。 建议添加标题特性。 基于 WCAG 2.0:H33、H40 和 H64。 Inspection ID: HtmlRequiredTitleAttribute", + "markdown": "报告缺少的标题特性 `frame`、`iframe`、`dl` 和 `a` 标记。 建议添加标题特性。 基于 WCAG 2.0:[H33](https://www.w3.org/TR/WCAG20-TECHS/H33.html)、[H40](https://www.w3.org/TR/WCAG20-TECHS/H40.html) 和 [H64](https://www.w3.org/TR/WCAG20-TECHS/H64.html)。\n\nInspection ID: HtmlRequiredTitleAttribute" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "HtmlRequiredTitleAttribute", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "HTML/可访问性", + "index": 33, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlDeprecatedTag", + "shortDescription": { + "text": "废弃的标记" + }, + "fullDescription": { + "text": "报告过时的 HTML5 标记。 建议将过时的标记替换为 CSS 或其他标记。 Inspection ID: HtmlDeprecatedTag", + "markdown": "报告过时的 HTML5 标记。 建议将过时的标记替换为 CSS 或其他标记。\n\nInspection ID: HtmlDeprecatedTag" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "HtmlDeprecatedTag", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CheckImageSize", + "shortDescription": { + "text": "不匹配的图像大小" + }, + "fullDescription": { + "text": "报告与所引用图像的实际宽度和高度不同的 'img' 标记的 'width' 和 'height' 特性值。 Inspection ID: CheckImageSize", + "markdown": "报告与所引用图像的实际宽度和高度不同的 `img` 标记的 `width` 和 `height` 特性值。\n\nInspection ID: CheckImageSize" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CheckImageSize", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlRequiredSummaryAttribute", + "shortDescription": { + "text": "缺少所需的 'summary' 特性" + }, + "fullDescription": { + "text": "报告 'table' 标记中缺少的 'summary' 特性。 建议添加 'summary' 特性。 基于 WCAG 2.0:H73。 Inspection ID: HtmlRequiredSummaryAttribute", + "markdown": "报告 `table` 标记中缺少的 `summary` 特性。 建议添加 `summary` 特性。 基于 WCAG 2.0:[H73](https://www.w3.org/TR/WCAG20-TECHS/H73.html)。\n\nInspection ID: HtmlRequiredSummaryAttribute" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "HtmlRequiredSummaryAttribute", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "HTML/可访问性", + "index": 33, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlRequiredLangAttribute", + "shortDescription": { + "text": "缺少所需的 'lang' 特性" + }, + "fullDescription": { + "text": "报告 'html' 标记中缺少的 'lang'(或 'xml:lang')特性。 建议添加一项必备特性来声明文档的默认语言。 基于 WCAG 2.0:H57。 Inspection ID: HtmlRequiredLangAttribute", + "markdown": "报告 `html` 标记中缺少的 `lang`(或 `xml:lang`)特性。 建议添加一项必备特性来声明文档的默认语言。 基于 WCAG 2.0:[H57](https://www.w3.org/TR/WCAG20-TECHS/H57.html)。\n\nInspection ID: HtmlRequiredLangAttribute" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "HtmlRequiredLangAttribute", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "HTML/可访问性", + "index": 33, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlNonExistentInternetResource", + "shortDescription": { + "text": "未解析的 Web 链接" + }, + "fullDescription": { + "text": "报告未解析的 Web 链接。 通过在后台发出网络请求来发挥作用。 Inspection ID: HtmlNonExistentInternetResource", + "markdown": "报告未解析的 Web 链接。 通过在后台发出网络请求来发挥作用。\n\nInspection ID: HtmlNonExistentInternetResource" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "HtmlNonExistentInternetResource", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlRequiredTitleElement", + "shortDescription": { + "text": "缺少所需的 'title' 元素" + }, + "fullDescription": { + "text": "报告 'head' 部分中缺少的 'title' 元素。 建议添加 'title' 元素。 标题应能说明文档。 基于 WCAG 2.0:H25。 Inspection ID: HtmlRequiredTitleElement", + "markdown": "报告 `head` 部分中缺少的 `title` 元素。 建议添加 `title` 元素。 标题应能说明文档。 基于 WCAG 2.0:[H25](https://www.w3.org/TR/WCAG20-TECHS/H25.html)。\n\nInspection ID: HtmlRequiredTitleElement" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "HtmlRequiredTitleElement", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "HTML/可访问性", + "index": 33, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlDeprecatedAttribute", + "shortDescription": { + "text": "废弃的特性" + }, + "fullDescription": { + "text": "报告过时的 HTML5 特性。 Inspection ID: HtmlDeprecatedAttribute", + "markdown": "报告过时的 HTML5 特性。\n\nInspection ID: HtmlDeprecatedAttribute" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "HtmlDeprecatedAttribute", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "HtmlPresentationalElement", + "shortDescription": { + "text": "演示标记" + }, + "fullDescription": { + "text": "报告 HTML 表示标记。 建议将表示标记替换为 CSS 或其他标记。 Inspection ID: HtmlPresentationalElement", + "markdown": "报告 HTML 表示标记。 建议将表示标记替换为 CSS 或其他标记。\n\nInspection ID: HtmlPresentationalElement" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "HtmlPresentationalElement", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "HTML", + "index": 27, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "com.intellij.css", + "version": "261.26222.73", + "rules": [ + { + "id": "CssInvalidHtmlTagReference", + "shortDescription": { + "text": "无效类型选择器" + }, + "fullDescription": { + "text": "报告与未知 HTML 元素匹配的 CSS 类型选择器。 Inspection ID: CssInvalidHtmlTagReference", + "markdown": "报告与未知 HTML 元素匹配的 CSS [类型选择器](https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors)。\n\nInspection ID: CssInvalidHtmlTagReference" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CssInvalidHtmlTagReference", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssInvalidCustomPropertyAtRuleDeclaration", + "shortDescription": { + "text": "@property 声明无效" + }, + "fullDescription": { + "text": "报告自定义属性的声明中缺少的必需语法、继承或 initial-value 属性。 Inspection ID: CssInvalidCustomPropertyAtRuleDeclaration", + "markdown": "报告自定义属性的声明中缺少的必需[语法](https://developer.mozilla.org/en-US/docs/web/css/@property/syntax)、[继承](https://developer.mozilla.org/en-US/docs/web/css/@property/inherits)或 [initial-value](https://developer.mozilla.org/en-US/docs/web/css/@property/initial-value) 属性。\n\nInspection ID: CssInvalidCustomPropertyAtRuleDeclaration" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CssInvalidCustomPropertyAtRuleDeclaration", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssInvalidFunction", + "shortDescription": { + "text": "无效函数" + }, + "fullDescription": { + "text": "报告未知的 CSS 函数或不正确的函数形参。 Inspection ID: CssInvalidFunction", + "markdown": "报告未知的 [CSS 函数](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions)或不正确的函数形参。\n\nInspection ID: CssInvalidFunction" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CssInvalidFunction", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssMissingSemicolon", + "shortDescription": { + "text": "缺少分号" + }, + "fullDescription": { + "text": "报告声明末尾处缺少的分号。 Inspection ID: CssMissingSemicolon", + "markdown": "报告声明末尾处缺少的分号。\n\nInspection ID: CssMissingSemicolon" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CssMissingSemicolon", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/代码样式问题", + "index": 49, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssRedundantUnit", + "shortDescription": { + "text": "冗余度量单位" + }, + "fullDescription": { + "text": "在规范不要求提供单位的情况下,报告值为零的度量单位。 示例: 'width: 0px' Inspection ID: CssRedundantUnit", + "markdown": "在规范不要求提供单位的情况下,报告值为零的度量单位。\n\n**示例:**\n\n width: 0px\n\nInspection ID: CssRedundantUnit" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CssRedundantUnit", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/代码样式问题", + "index": 49, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssMissingComma", + "shortDescription": { + "text": "选择器列表中缺少逗号" + }, + "fullDescription": { + "text": "报告多行选择器。 这很可能表明实际上要使用多个单行选择器,但有一行或几行的末尾缺少逗号。 示例: 'input /* comma has probably been forgotten */\n.button {\n margin: 1px;\n}' Inspection ID: CssMissingComma", + "markdown": "报告多行选择器。 这很可能表明实际上要使用多个单行选择器,但有一行或几行的末尾缺少逗号。\n\n**示例:**\n\n\n input /* comma has probably been forgotten */\n .button {\n margin: 1px;\n }\n\nInspection ID: CssMissingComma" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CssMissingComma", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/可能的 bug", + "index": 57, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssInvalidPropertyValue", + "shortDescription": { + "text": "无效的属性值" + }, + "fullDescription": { + "text": "报告不正确的 CSS 属性值。 Inspection ID: CssInvalidPropertyValue", + "markdown": "报告不正确的 CSS 属性值。\n\nInspection ID: CssInvalidPropertyValue" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CssInvalidPropertyValue", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssBrowserCompatibilityForProperties", + "shortDescription": { + "text": "属性与所选浏览器不兼容" + }, + "fullDescription": { + "text": "报告不受指定浏览器支持的 CSS 属性。 基于 MDN 兼容性数据。 Inspection ID: CssBrowserCompatibilityForProperties", + "markdown": "报告不受指定浏览器支持的 CSS 属性。 基于 [MDN 兼容性数据](https://github.com/mdn/browser-compat-data)。\n\nInspection ID: CssBrowserCompatibilityForProperties" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CssBrowserCompatibilityForProperties", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "CSS", + "index": 41, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssInvalidCustomPropertyAtRuleName", + "shortDescription": { + "text": "@property 名称无效" + }, + "fullDescription": { + "text": "报告无效的自定义属性名称。 自定义属性名称应以两个短划线作为前缀。 示例: '@property invalid-property-name {\n ...\n}\n\n@property --valid-property-name {\n ...\n}' Inspection ID: CssInvalidCustomPropertyAtRuleName", + "markdown": "报告无效的自定义属性名称。 自定义属性名称应以两个短划线作为前缀。\n\n**示例:**\n\n\n @property invalid-property-name {\n ...\n }\n\n @property --valid-property-name {\n ...\n }\n\nInspection ID: CssInvalidCustomPropertyAtRuleName" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CssInvalidCustomPropertyAtRuleName", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssConvertColorToHexInspection", + "shortDescription": { + "text": "颜色可被替换为 #-hex" + }, + "fullDescription": { + "text": "报告 'rgb()'、'hsl()' 或其他颜色函数。 建议用等效的十六进制表示法代替颜色函数。 示例: 'rgb(12, 15, 255)' 在应用快速修复后: '#0c0fff'. Inspection ID: CssConvertColorToHexInspection", + "markdown": "报告 `rgb()`、`hsl()` 或其他颜色函数。\n\n建议用等效的十六进制表示法代替颜色函数。\n\n**示例:**\n\n rgb(12, 15, 255)\n\n在应用快速修复后:\n\n #0c0fff.\n\nInspection ID: CssConvertColorToHexInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CssConvertColorToHexInspection", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "CSS", + "index": 41, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssReplaceWithShorthandUnsafely", + "shortDescription": { + "text": "属性或许可被替换为速记形式" + }, + "fullDescription": { + "text": "报告一组 CSS 常规属性,并建议将不完整的一组 CSS 常规属性替换为速记形式,但在本例中,速记形式并非 100% 等效。 例如,'outline-color' 和 'outline-style' 这 2 个属性可以替换为单个 'outline'。 此类替换不是 100% 等效,因为速记形式会将所有忽略的子值重置为其初始状态。 在本例中,切换到 'outling' 简写形式意味着 'outling-width' 也将设置为其初始值,即 'medium'。 此检查不会处理整组常规属性(在切换到速记形式 100% 安全时)。 对于此类情况,请参阅“属性可以安全替换为速记形式”检查。 Inspection ID: CssReplaceWithShorthandUnsafely", + "markdown": "报告一组 CSS 常规属性,并建议将不完整的一组 CSS 常规属性替换为速记形式,但在本例中,速记形式并非 100% 等效。\n\n\n例如,`outline-color` 和 `outline-style` 这 2 个属性可以替换为单个 `outline`。\n此类替换不是 100% 等效,因为速记形式会将所有忽略的子值重置为其初始状态。\n在本例中,切换到 `outling` 简写形式意味着 `outling-width` 也将设置为其初始值,即 `medium`。\n\n\n此检查不会处理整组常规属性(在切换到速记形式 100% 安全时)。\n对于此类情况,请参阅\"属性可以安全替换为速记形式\"检查。\n\nInspection ID: CssReplaceWithShorthandUnsafely" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "CssReplaceWithShorthandUnsafely", + "ideaSeverity": "INFORMATION", + "qodanaSeverity": "Info", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "CSS", + "index": 41, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssUnknownUnit", + "shortDescription": { + "text": "未知单位" + }, + "fullDescription": { + "text": "报告未知单元. Inspection ID: CssUnknownUnit", + "markdown": "报告未知单元.\n\nInspection ID: CssUnknownUnit" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CssUnknownUnit", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssInvalidMediaFeature", + "shortDescription": { + "text": "无效媒体特性" + }, + "fullDescription": { + "text": "报告未知 CSS 媒体特性或不正确的媒体特性值。 Inspection ID: CssInvalidMediaFeature", + "markdown": "报告未知 [CSS 媒体特性](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries)或不正确的媒体特性值。\n\nInspection ID: CssInvalidMediaFeature" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CssInvalidMediaFeature", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssConvertColorToRgbInspection", + "shortDescription": { + "text": "颜色可被替换为 rgb()" + }, + "fullDescription": { + "text": "报告 'hsl()' 或 'hwb()' 颜色函数或十六进制的颜色表示法。 建议将此类颜色值替换为等效的 'rgb()' 或 'rgba()' 颜色函数。 示例: '#0c0fff' 在应用快速修复后: 'rgb(12, 15, 255)'. Inspection ID: CssConvertColorToRgbInspection", + "markdown": "报告 `hsl()` 或 `hwb()` 颜色函数或十六进制的颜色表示法。\n\n建议将此类颜色值替换为等效的 `rgb()` 或 `rgba()` 颜色函数。\n\n**示例:**\n\n #0c0fff\n\n在应用快速修复后:\n\n rgb(12, 15, 255).\n\nInspection ID: CssConvertColorToRgbInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CssConvertColorToRgbInspection", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "CSS", + "index": 41, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssUnusedSymbol", + "shortDescription": { + "text": "未使用的选择器" + }, + "fullDescription": { + "text": "报告出现在选择器中但未在 HTML 中使用的 CSS 类或元素 ID。 请注意,只有通过代码 | 检查代码或代码 | 分析代码 | 按名称运行检查运行时,才能获得完整的检查结果。 由于性能原因,不实时检查样式表文件。 Inspection ID: CssUnusedSymbol", + "markdown": "报告出现在选择器中但未在 HTML 中使用的 CSS 类或元素 ID。\n\n\n请注意,只有通过**代码 \\| 检查代码** 或**代码 \\| 分析代码 \\| 按名称运行检查**运行时,才能获得完整的检查结果。\n由于性能原因,不实时检查样式表文件。\n\nInspection ID: CssUnusedSymbol" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CssUnusedSymbol", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "CSS", + "index": 41, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssDeprecatedValue", + "shortDescription": { + "text": "弃用的值" + }, + "fullDescription": { + "text": "报告已弃用的 CSS 值。 建议将已弃用的值替换为其有效的等效值。 Inspection ID: CssDeprecatedValue", + "markdown": "报告已弃用的 CSS 值。 建议将已弃用的值替换为其有效的等效值。\n\nInspection ID: CssDeprecatedValue" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CssDeprecatedValue", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "CSS", + "index": 41, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssNonIntegerLengthInPixels", + "shortDescription": { + "text": "非整数长度(像素)" + }, + "fullDescription": { + "text": "报告以像素为单位的非整数长度。 示例: 'width: 3.14px' Inspection ID: CssNonIntegerLengthInPixels", + "markdown": "报告以像素为单位的非整数长度。\n\n**示例:**\n\n width: 3.14px\n\nInspection ID: CssNonIntegerLengthInPixels" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "CssNonIntegerLengthInPixels", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/可能的 bug", + "index": 57, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssInvalidImport", + "shortDescription": { + "text": "@import 位置错误" + }, + "fullDescription": { + "text": "报告位置错误的 '@import' 语句。 根据规范,'@import' 规则必须在样式表的顶部定义,位于任何其他 at-rule('@charset' 和 '@layer' 除外)和样式声明之前,否则将被忽略。 Inspection ID: CssInvalidImport", + "markdown": "报告位置错误的 `@import` 语句。\n\n\n根据[规范](https://developer.mozilla.org/en-US/docs/Web/CSS/@import),`@import` 规则必须在样式表的顶部定义,位于任何其他 at-rule(`@charset` 和 `@layer` 除外)和样式声明之前,否则将被忽略。\n\nInspection ID: CssInvalidImport" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CssInvalidImport", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssInvalidAtRule", + "shortDescription": { + "text": "未知 @ 规则" + }, + "fullDescription": { + "text": "报告未知的 CSS @ 规则。 Inspection ID: CssInvalidAtRule", + "markdown": "报告未知的 [CSS @ 规则](https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule)。\n\nInspection ID: CssInvalidAtRule" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CssInvalidAtRule", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssUnresolvedCustomProperty", + "shortDescription": { + "text": "未解析的自定义属性" + }, + "fullDescription": { + "text": "报告对 'var()' 函数实参中的自定义属性的未解析引用。 Inspection ID: CssUnresolvedCustomProperty", + "markdown": "报告对 `var()` 函数实参中的[自定义属性](https://developer.mozilla.org/en-US/docs/Web/CSS/--*)的未解析引用。\n\nInspection ID: CssUnresolvedCustomProperty" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CssUnresolvedCustomProperty", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssOverwrittenProperties", + "shortDescription": { + "text": "覆盖的属性" + }, + "fullDescription": { + "text": "报告规则集中重复的 CSS 属性。 遵循速记属性。 示例: '.foo {\n margin-bottom: 1px;\n margin-bottom: 1px; /* duplicates margin-bottom */\n margin: 0; /* overrides margin-bottom */\n}' Inspection ID: CssOverwrittenProperties", + "markdown": "报告规则集中重复的 CSS 属性。 遵循速记属性。\n\n**示例:**\n\n\n .foo {\n margin-bottom: 1px;\n margin-bottom: 1px; /* duplicates margin-bottom */\n margin: 0; /* overrides margin-bottom */\n }\n\nInspection ID: CssOverwrittenProperties" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CssOverwrittenProperties", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "CSS", + "index": 41, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssUnknownTarget", + "shortDescription": { + "text": "未解析的文件引用" + }, + "fullDescription": { + "text": "报告未解析的文件引用,例如 '@import' 语句中的不正确路径。 Inspection ID: CssUnknownTarget", + "markdown": "报告未解析的文件引用,例如 `@import` 语句中的不正确路径。\n\nInspection ID: CssUnknownTarget" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CssUnknownTarget", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssNegativeValue", + "shortDescription": { + "text": "负属性值" + }, + "fullDescription": { + "text": "报告应不小于零的 CSS 负值属性(例如对象的宽度或高度)。 Inspection ID: CssNegativeValue", + "markdown": "报告应不小于零的 CSS 负值属性(例如对象的宽度或高度)。\n\nInspection ID: CssNegativeValue" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CssNegativeValue", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssNoGenericFontName", + "shortDescription": { + "text": "缺少通用字体系列名称" + }, + "fullDescription": { + "text": "验证 'font-family' 属性是否包含通用的字体系列名称作为回退备选项。 通用字体系列名称包括:'serif'、'sans-serif'、'cursive'、'fantasy' 和 'monospace'。 Inspection ID: CssNoGenericFontName", + "markdown": "验证 [font-family](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family) 属性是否包含通用的字体系列名称作为回退备选项。\n\n\n通用字体系列名称包括:`serif`、`sans-serif`、`cursive`、`fantasy` 和 `monospace`。\n\nInspection ID: CssNoGenericFontName" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CssNoGenericFontName", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/可能的 bug", + "index": 57, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssUnresolvedClassInComposesRule", + "shortDescription": { + "text": "'composes' 规则中的未解析类" + }, + "fullDescription": { + "text": "报告 'composes' 规则中无法解析为任何有效目标的 CSS 类引用。 示例: '.className {/* ... */}\n\n .otherClassName {\n composes: className;\n }' Inspection ID: CssUnresolvedClassInComposesRule", + "markdown": "报告 ['composes'](https://github.com/css-modules/css-modules#composition) 规则中无法解析为任何有效目标的 CSS 类引用。\n\n**示例:**\n\n\n .className {/* ... */}\n\n .otherClassName {\n composes: className;\n }\n\nInspection ID: CssUnresolvedClassInComposesRule" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CssUnresolvedClassInComposesRule", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssInvalidCharsetRule", + "shortDescription": { + "text": "@charset 位置错误或不正确" + }, + "fullDescription": { + "text": "报告位置错误的 '@charset' @ 规则或不正确的字符集值。 Inspection ID: CssInvalidCharsetRule", + "markdown": "报告位置错误的 `@charset` @ 规则或不正确的字符集值。\n\nInspection ID: CssInvalidCharsetRule" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CssInvalidCharsetRule", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssReplaceWithShorthandSafely", + "shortDescription": { + "text": "属性可以安全地替换为速记形式" + }, + "fullDescription": { + "text": "报告一组普通属性。 建议将整组 CSS 常规属性替换为等效的速记形式。 例如,'padding-top'、'padding-right'、'padding-bottom' 和 'padding-left' 这 4 个属性可以安全地替换为单个 'padding' 属性。 注意,如果这组常规属性不完整(例如,规则集中只有 3 个 'padding-xxx' 属性),则不会显示此检查,因为切换到速记形式可能会改变结果。 对于此类情况,可以考虑执行“属性可能已被替换为速记形式”检查。 Inspection ID: CssReplaceWithShorthandSafely", + "markdown": "报告一组普通属性。 建议将整组 CSS 常规属性替换为等效的速记形式。\n\n\n例如,`padding-top`、`padding-right`、`padding-bottom` 和 `padding-left`\n这 4 个属性可以安全地替换为单个 `padding` 属性。\n\n\n注意,如果这组常规属性不完整(例如,规则集中只有 3 个 `padding-xxx` 属性),则不会显示此检查,因为切换到速记形式可能会改变结果。\n对于此类情况,可以考虑执行\"属性可能已被替换为速记形式\"检查。\n\nInspection ID: CssReplaceWithShorthandSafely" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "CssReplaceWithShorthandSafely", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "CSS", + "index": 41, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssUnknownProperty", + "shortDescription": { + "text": "未知属性" + }, + "fullDescription": { + "text": "报告未知的 CSS 属性或在错误的上下文中使用的属性。 将未知属性添加到“自定义 CSS 属性”列表以跳过验证。 Inspection ID: CssUnknownProperty", + "markdown": "报告未知的 CSS 属性或在错误的上下文中使用的属性。\n\n将未知属性添加到\"自定义 CSS 属性\"列表以跳过验证。\n\nInspection ID: CssUnknownProperty" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CssUnknownProperty", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssInvalidPseudoSelector", + "shortDescription": { + "text": "无效的伪选择器" + }, + "fullDescription": { + "text": "报告不正确的 CSS pseudo-class pseudo-element。 Inspection ID: CssInvalidPseudoSelector", + "markdown": "报告不正确的 CSS [pseudo-class](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes) [pseudo-element](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements)。\n\nInspection ID: CssInvalidPseudoSelector" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CssInvalidPseudoSelector", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CssInvalidNestedSelector", + "shortDescription": { + "text": "嵌套选择器无效" + }, + "fullDescription": { + "text": "报告以标识符或函数表示法开头的嵌套选择器。 Inspection ID: CssInvalidNestedSelector", + "markdown": "报告以标识符或函数表示法开头的嵌套选择器。\n\nInspection ID: CssInvalidNestedSelector" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CssInvalidNestedSelector", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/无效元素", + "index": 42, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "org.jetbrains.plugins.yaml", + "version": "261.26222.73", + "rules": [ + { + "id": "YAMLSchemaValidation", + "shortDescription": { + "text": "由 JSON 架构验证" + }, + "fullDescription": { + "text": "如果指定了架构,则报告 YAML 文件和 JSON 架构之间的不一致。 方案示例: '{\n \"properties\": {\n \"SomeNumberProperty\": {\n \"type\": \"number\"\n }\n }\n }' 以下是带有相应警告的示例: 'SomeNumberProperty: hello world' Inspection ID: YAMLSchemaValidation", + "markdown": "如果指定了架构,则报告 YAML 文件和 JSON 架构之间的不一致。\n\n**方案示例:**\n\n\n {\n \"properties\": {\n \"SomeNumberProperty\": {\n \"type\": \"number\"\n }\n }\n }\n\n**以下是带有相应警告的示例:**\n\n\n SomeNumberProperty: hello world\n\nInspection ID: YAMLSchemaValidation" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "YAMLSchemaValidation", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "YAML", + "index": 46, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "YAMLIncompatibleTypes", + "shortDescription": { + "text": "可疑的类型不匹配" + }, + "fullDescription": { + "text": "报告 YAML 文件中的标量值类型与相似位置中的值类型不匹配。 示例: 'myElements:\n - value1\n - value2\n - false # <- 已报告,因为它是布尔值,而其他值是字符串' Inspection ID: YAMLIncompatibleTypes", + "markdown": "报告 YAML 文件中的标量值类型与相似位置中的值类型不匹配。\n\n**示例:**\n\n\n myElements:\n - value1\n - value2\n - false # <- 已报告,因为它是布尔值,而其他值是字符串\n\nInspection ID: YAMLIncompatibleTypes" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "YAMLIncompatibleTypes", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "YAML", + "index": 46, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "YAMLUnresolvedAlias", + "shortDescription": { + "text": "未解析的别名" + }, + "fullDescription": { + "text": "报告 YAML 文件中的未解析别名。 示例: 'some_key: *unknown_alias' Inspection ID: YAMLUnresolvedAlias", + "markdown": "报告 YAML 文件中的未解析别名。\n\n**示例:**\n\n\n some_key: *unknown_alias\n\nInspection ID: YAMLUnresolvedAlias" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "YAMLUnresolvedAlias", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "YAML", + "index": 46, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "YAMLSchemaDeprecation", + "shortDescription": { + "text": "弃用的 YAML 键" + }, + "fullDescription": { + "text": "报告 YAML 文件中已弃用的键。 仅当存在与相应 YAML 文件关联的 JSON 架构时,才会检查是否弃用。 请注意,JSON 架构规范中尚未定义弃用机制,并且此检查使用非标准的 'deprecationMessage' 扩展。 方案弃用示例: '{\n \"properties\": {\n \"SomeDeprecatedProperty\": {\n \"deprecationMessage\": \"Baz\",\n \"description\": \"Foo bar\"\n }\n }\n }' 以下是带有相应警告的示例: 'SomeDeprecatedProperty: some value' Inspection ID: YAMLSchemaDeprecation", + "markdown": "报告 YAML 文件中已弃用的键。\n\n仅当存在与相应 YAML 文件关联的 JSON 架构时,才会检查是否弃用。\n\n请注意,JSON 架构规范中尚未定义弃用机制,并且此检查使用非标准的 `deprecationMessage` 扩展。\n\n**方案弃用示例:**\n\n\n {\n \"properties\": {\n \"SomeDeprecatedProperty\": {\n \"deprecationMessage\": \"Baz\",\n \"description\": \"Foo bar\"\n }\n }\n }\n\n**以下是带有相应警告的示例:**\n\n\n SomeDeprecatedProperty: some value\n\nInspection ID: YAMLSchemaDeprecation" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "YAMLSchemaDeprecation", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "YAML", + "index": 46, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "YAMLRecursiveAlias", + "shortDescription": { + "text": "递归别名" + }, + "fullDescription": { + "text": "报告 YAML 别名中的递归。 别名不能递归并在相应定位标记引用的数据中使用。 示例: 'some_key: &some_anchor\n sub_key1: value1\n sub_key2: *some_anchor' Inspection ID: YAMLRecursiveAlias", + "markdown": "报告 YAML 别名中的递归。\n\n别名不能递归并在相应定位标记引用的数据中使用。\n\n**示例:**\n\n\n some_key: &some_anchor\n sub_key1: value1\n sub_key2: *some_anchor\n\nInspection ID: YAMLRecursiveAlias" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "YAMLRecursiveAlias", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "YAML", + "index": 46, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "YAMLDuplicatedKeys", + "shortDescription": { + "text": "重复的 YAML 键" + }, + "fullDescription": { + "text": "报告 YAML 文件中的重复键。 示例: 'same_key: some value\n same_key: another value' Inspection ID: YAMLDuplicatedKeys", + "markdown": "报告 YAML 文件中的重复键。\n\n**示例:**\n\n\n same_key: some value\n same_key: another value\n\nInspection ID: YAMLDuplicatedKeys" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "YAMLDuplicatedKeys", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "YAML", + "index": 46, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "YAMLUnusedAnchor", + "shortDescription": { + "text": "未使用的定位标记" + }, + "fullDescription": { + "text": "报告未使用的定位标记。 示例: 'some_key: &some_anchor\n key1: value1' Inspection ID: YAMLUnusedAnchor", + "markdown": "报告未使用的定位标记。\n\n**示例:**\n\n\n some_key: &some_anchor\n key1: value1\n\nInspection ID: YAMLUnusedAnchor" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "YAMLUnusedAnchor", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "YAML", + "index": 46, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "org.jetbrains.plugins.less", + "version": "261.26222.73", + "rules": [ + { + "id": "LessUnresolvedMixin", + "shortDescription": { + "text": "未解析的 mixin" + }, + "fullDescription": { + "text": "报告对未解析的 Less mixin 的引用。 示例: '* {\n .unknown-mixin();\n}' Inspection ID: LessUnresolvedMixin", + "markdown": "报告对未解析的 [Less mixin](http://lesscss.org/features/#mixins-feature) 的引用。\n\n**示例:**\n\n\n * {\n .unknown-mixin();\n }\n\nInspection ID: LessUnresolvedMixin" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "LessUnresolvedMixin", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Less", + "index": 50, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "LessUnresolvedVariable", + "shortDescription": { + "text": "未解析的变量" + }, + "fullDescription": { + "text": "报告对未解析的 Less 变量的引用。 示例: '* {\n margin: @unknown-var;\n}' Inspection ID: LessUnresolvedVariable", + "markdown": "报告对未解析的 [Less 变量](http://lesscss.org/features/#variables-feature)的引用。\n\n**示例:**\n\n\n * {\n margin: @unknown-var;\n }\n\nInspection ID: LessUnresolvedVariable" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "LessUnresolvedVariable", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Less", + "index": 50, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "LessResolvedByNameOnly", + "shortDescription": { + "text": "缺少 import" + }, + "fullDescription": { + "text": "报告对在另一个文件中已声明但未在当前文件中显式导入的变量或 mixin 的引用。 示例: '* {\n margin: @var-in-other-file;\n}' Inspection ID: LessResolvedByNameOnly", + "markdown": "报告对在另一个文件中已声明但未在当前文件中显式[导入](http://lesscss.org/features/#import-atrules-feature)的变量或 mixin 的引用。\n\n**示例:**\n\n\n * {\n margin: @var-in-other-file;\n }\n\nInspection ID: LessResolvedByNameOnly" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "LessResolvedByNameOnly", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Less", + "index": 50, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "org.jetbrains.plugins.vue", + "version": "261.26222.73", + "rules": [ + { + "id": "VueDataFunction", + "shortDescription": { + "text": "数据函数" + }, + "fullDescription": { + "text": "报告并非函数的 Vue 组件 data 属性。 建议用函数包装对象字面量。 在定义组件时,'data' 必须声明为返回初始数据对象的函数,因为将使用相同的定义创建多个实例。 如果 'data' 仍然使用普通对象,那么该对象将通过引用来在创建的所有实例中共享! 有了 'data' 函数,每次创建新实例时,都可以直接调用它以返回初始数据的新副本。 Inspection ID: VueDataFunction", + "markdown": "报告并非函数的 Vue 组件 [data](https://vuejs.org/v2/api/#data) 属性。 建议用函数包装对象字面量。\n\n在定义组件时,`data` 必须声明为返回初始数据对象的函数,因为将使用相同的定义创建多个实例。 如果 `data` 仍然使用普通对象,那么该对象将通过引用来在创建的所有实例中共享! 有了 `data` 函数,每次创建新实例时,都可以直接调用它以返回初始数据的新副本。\n\nInspection ID: VueDataFunction" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "VueDataFunction", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Vue", + "index": 52, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "VueUnrecognizedSlot", + "shortDescription": { + "text": "无法识别的槽位" + }, + "fullDescription": { + "text": "报告无法识别的 Vue 插槽。 Inspection ID: VueUnrecognizedSlot", + "markdown": "报告无法识别的 Vue 插槽。\n\nInspection ID: VueUnrecognizedSlot" + }, + "defaultConfiguration": { + "enabled": false, + "level": "note", + "parameters": { + "suppressToolId": "VueUnrecognizedSlot", + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Vue", + "index": 52, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "VueMissingComponentImportInspection", + "shortDescription": { + "text": "缺少组件导入" + }, + "fullDescription": { + "text": "报告需要在 Vue 模板中导入的 Vue 组件。 它提供了一个快速修复来添加缺失的导入。 Inspection ID: VueMissingComponentImportInspection", + "markdown": "报告需要在 Vue 模板中导入的 Vue 组件。 它提供了一个快速修复来添加缺失的导入。\n\nInspection ID: VueMissingComponentImportInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "VueMissingComponentImportInspection", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Vue", + "index": 52, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "VueUnrecognizedDirective", + "shortDescription": { + "text": "无法识别的指令" + }, + "fullDescription": { + "text": "报告无法识别的 Vue 指令。 Inspection ID: VueUnrecognizedDirective", + "markdown": "报告无法识别的 Vue 指令。\n\nInspection ID: VueUnrecognizedDirective" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "VueUnrecognizedDirective", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "Vue", + "index": 52, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "VueDuplicateTag", + "shortDescription": { + "text": "重复的模板/脚本标记" + }, + "fullDescription": { + "text": "报告 Vue 文件中 'template' 或 'script' 标记的多次用法。 Vue 组件规范指示每个 '*.vue' 文件一次最多只能包含一个 'template' 或 'script' 块。 Inspection ID: VueDuplicateTag", + "markdown": "报告 Vue 文件中 `template` 或 `script` 标记的多次用法。\n\n[Vue 组件规范](https://vue-loader.vuejs.org/spec.html)指示每个 `*.vue` 文件一次最多只能包含一个 `template` 或 `script` 块。\n\nInspection ID: VueDuplicateTag" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "VueDuplicateTag", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "Vue", + "index": 52, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "VueDeprecatedSymbol", + "shortDescription": { + "text": "已弃用的符号" + }, + "fullDescription": { + "text": "报告已弃用的 Vue 符号。 Inspection ID: VueDeprecatedSymbol", + "markdown": "报告已弃用的 Vue 符号。\n\nInspection ID: VueDeprecatedSymbol" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "VueDeprecatedSymbol", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "Vue", + "index": 52, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "org.intellij.qodana", + "version": "261.26222.73", + "rules": [ + { + "id": "JsCoverageInspection", + "shortDescription": { + "text": "检查 JavaScript 和 TypeScript 源代码覆盖率" + }, + "fullDescription": { + "text": "报告覆盖率低于某个阈值的方法、类和文件。 Inspection ID: JsCoverageInspection", + "markdown": "报告覆盖率低于某个阈值的方法、类和文件。\n\nInspection ID: JsCoverageInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JsCoverageInspection", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "代码覆盖率", + "index": 63, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CyclomaticComplexityInspection", + "shortDescription": { + "text": "代码指标" + }, + "fullDescription": { + "text": "计算循环复杂度。 Inspection ID: CyclomaticComplexityInspection", + "markdown": "计算循环复杂度。\n\nInspection ID: CyclomaticComplexityInspection" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "CyclomaticComplexityInspection", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Qodana", + "index": 66, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "org.jetbrains.plugins.github", + "version": "261.26222.73", + "rules": [ + { + "id": "GithubFunctionSignatureValidation", + "shortDescription": { + "text": "标准库函数验证" + }, + "fullDescription": { + "text": "报告无效的 GitHub Actions Expression 语言标准库函数调用 有关 GitHub Actions Expression 语言的详细信息,请参阅 GitHub 文档。 Inspection ID: GithubFunctionSignatureValidation", + "markdown": "报告无效的 GitHub Actions Expression 语言标准库函数调用\n\n\n有关 GitHub Actions Expression 语言的详细信息,请参阅 [GitHub 文档](https://docs.github.com/en/actions/learn-github-actions/expressions)。\n\nInspection ID: GithubFunctionSignatureValidation" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "GithubFunctionSignatureValidation", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "GitHub 操作", + "index": 64, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "IllegalJobDependency", + "shortDescription": { + "text": "未定义的作业依赖项" + }, + "fullDescription": { + "text": "检测 GitHub 工作流 YML 文件中未定义作业的依赖关系。 有关工作流语法的更多信息,请参阅 GitHub Actions 文档。 Inspection ID: IllegalJobDependency", + "markdown": "检测 GitHub 工作流 YML 文件中未定义作业的依赖关系。\n\n\n有关工作流语法的更多信息,请参阅 [GitHub Actions 文档](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds)。\n\nInspection ID: IllegalJobDependency" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "IllegalJobDependency", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "GitHub 操作", + "index": 64, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "CyclicJobDependency", + "shortDescription": { + "text": "循环作业依赖关系" + }, + "fullDescription": { + "text": "检测 GitHub 工作流 YML 文件中作业的循环依赖关系。 有关工作流语法的更多信息,请参阅 GitHub Actions 文档。 Inspection ID: CyclicJobDependency", + "markdown": "检测 GitHub 工作流 YML 文件中作业的循环依赖关系。\n\n\n有关工作流语法的更多信息,请参阅 [GitHub Actions 文档](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds)。\n\nInspection ID: CyclicJobDependency" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "CyclicJobDependency", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "GitHub 操作", + "index": 64, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "UndefinedParamsPresent", + "shortDescription": { + "text": "未定义的形参" + }, + "fullDescription": { + "text": "报告是否存在未在操作中定义的形参。 它还通过移除未定义的形参提供快速修复。 有关操作形参的详细信息,请参阅 GitHub 文档。 Inspection ID: UndefinedParamsPresent", + "markdown": "报告是否存在未在操作中定义的形参。 它还通过移除未定义的形参提供快速修复。\n\n\n有关操作形参的详细信息,请参阅 [GitHub 文档](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsstepswith)。\n\nInspection ID: UndefinedParamsPresent" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "UndefinedParamsPresent", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "GitHub 操作", + "index": 64, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "MandatoryParamsAbsent", + "shortDescription": { + "text": "形参无效" + }, + "fullDescription": { + "text": "报告缺少无操作默认值的强制形参的情况。 它还通过用空值添加缺少的形参来提供快速修复。 有关操作形参的详细信息,请参阅 GitHub 文档。 Inspection ID: MandatoryParamsAbsent", + "markdown": "报告缺少无操作默认值的强制形参的情况。 它还通过用空值添加缺少的形参来提供快速修复。\n\n\n有关操作形参的详细信息,请参阅 [GitHub 文档](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsstepswith)。\n\nInspection ID: MandatoryParamsAbsent" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "MandatoryParamsAbsent", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "GitHub 操作", + "index": 64, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "UndefinedAction", + "shortDescription": { + "text": "未定义的操作/文件引用" + }, + "fullDescription": { + "text": "检测 GitHub 操作和工作流文件中未解析的操作引用。 有关操作引用的详细信息,请参阅 GitHub文档。 Inspection ID: UndefinedAction", + "markdown": "检测 GitHub 操作和工作流文件中未解析的操作引用。\n\n\n有关操作引用的详细信息,请参阅 [GitHub文档](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsuses)。\n\nInspection ID: UndefinedAction" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "UndefinedAction", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "GitHub 操作", + "index": 64, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "org.jetbrains.plugins.gitlab", + "version": "261.26222.73", + "rules": [ + { + "id": "DuplicatedJobUsage", + "shortDescription": { + "text": "重复的作业用法" + }, + "fullDescription": { + "text": "检测 GitLab CI/CD 配置文件中重复的作业用法。 有关作业引用的更多信息,请参阅 GitLab 文档。 Inspection ID: DuplicatedJobUsage", + "markdown": "检测 GitLab CI/CD 配置文件中重复的作业用法。\n\n\n有关作业引用的更多信息,请参阅 [GitLab 文档](https://docs.gitlab.com/ee/ci/jobs/index.html)。\n\nInspection ID: DuplicatedJobUsage" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "DuplicatedJobUsage", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "GitLab CI_CD", + "index": 65, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "UndefinedStage", + "shortDescription": { + "text": "未定义的暂存" + }, + "fullDescription": { + "text": "检测 GitLab CI/CD 配置文件中未解析的暂存引用。 有关暂存引用的更多信息,请参阅 GitLab 文档。 Inspection ID: UndefinedStage", + "markdown": "检测 GitLab CI/CD 配置文件中未解析的暂存引用。\n\n\n有关暂存引用的更多信息,请参阅 [GitLab 文档](https://docs.gitlab.com/ee/ci/yaml/index.html#stages)。\n\nInspection ID: UndefinedStage" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "UndefinedStage", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "GitLab CI_CD", + "index": 65, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "UndefinedJob", + "shortDescription": { + "text": "未定义的作业" + }, + "fullDescription": { + "text": "检测 GitLab CI/CD 配置文件中未解析的作业引用。 有关作业引用的更多信息,请参阅 GitLab 文档。 Inspection ID: UndefinedJob", + "markdown": "检测 GitLab CI/CD 配置文件中未解析的作业引用。\n\n\n有关作业引用的更多信息,请参阅 [GitLab 文档](https://docs.gitlab.com/ee/ci/jobs/index.html)。\n\nInspection ID: UndefinedJob" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "UndefinedJob", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "GitLab CI_CD", + "index": 65, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "com.intellij.stylelint", + "version": "261.26222.73", + "rules": [ + { + "id": "Stylelint", + "shortDescription": { + "text": "Stylelint" + }, + "fullDescription": { + "text": "报告 Stylelint linter 检测到的差异。 高亮显示基于 Stylelint 配置文件中为每条规则指定的规则严重性。 Inspection ID: Stylelint", + "markdown": "报告 [Stylelint](http://stylelint.io) linter 检测到的差异。 \n\n高亮显示基于 [Stylelint 配置文件](https://stylelint.io/user-guide/configure)中为每条规则指定的规则严重性。\n\nInspection ID: Stylelint" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "Stylelint", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Code Style" + } + }, + "relationships": [ + { + "target": { + "id": "CSS/代码质量工具", + "index": 67, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "intellij.webpack", + "version": "261.26222.73", + "rules": [ + { + "id": "WebpackConfigHighlighting", + "shortDescription": { + "text": "Webpack 配置符合 JSON 架构" + }, + "fullDescription": { + "text": "根据 webpack 选项架构验证 webpack 配置文件中的选项(名称应以 `webpack` 开头,例如 `webpack.config.js`)。 禁用此检查以关闭配置对象内部的验证和代码补全。 Inspection ID: WebpackConfigHighlighting", + "markdown": "根据 [webpack 选项架构](https://github.com/webpack/webpack/blob/master/schemas/WebpackOptions.json)验证 webpack 配置文件中的选项(名称应以 \\`webpack\\` 开头,例如 \\`webpack.config.js\\`)。 \n\n禁用此检查以关闭配置对象内部的验证和代码补全。\n\nInspection ID: WebpackConfigHighlighting" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "WebpackConfigHighlighting", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/常规", + "index": 18, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "tslint", + "version": "261.26222.73", + "rules": [ + { + "id": "TsLint", + "shortDescription": { + "text": "TSLint" + }, + "fullDescription": { + "text": "报告 TSLint linter 检测到的差异。 高亮显示基于 TSLint 配置文件中为每条规则指定的规则严重性。 清除“使用配置文件中的规则严重性”复选框,对所有 TSLint 规则使用该项检查中配置的严重性。 Inspection ID: TsLint", + "markdown": "报告 [TSLint](https://github.com/palantir/tslint) linter 检测到的差异。 \n\n高亮显示基于 [TSLint 配置文件](https://palantir.github.io/tslint/usage/configuration/)中为每条规则指定的规则严重性。 \n\n清除\"使用配置文件中的规则严重性\"复选框,对所有 TSLint 规则使用该项检查中配置的严重性。\n\nInspection ID: TsLint" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "TsLint", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "JavaScript 和 TypeScript/代码质量工具", + "index": 62, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "org.jetbrains.plugins.docker.gateway", + "version": "261.26222.73", + "rules": [ + { + "id": "DevcontainerFolder", + "shortDescription": { + "text": "Dev Container 文件夹结构问题" + }, + "fullDescription": { + "text": "检查嵌套的 .devcontainer 文件夹,查看其是否具有不明确的 Dev Container 上下文,或者是否在需要的地方缺少父级 .devcontainer 文件夹。 Inspection ID: DevcontainerFolder", + "markdown": "检查嵌套的 .devcontainer 文件夹,查看其是否具有不明确的 Dev Container 上下文,或者是否在需要的地方缺少父级 .devcontainer 文件夹。\n\nInspection ID: DevcontainerFolder" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "DevcontainerFolder", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Dev Container", + "index": 70, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "DevContainerIdeSettings", + "shortDescription": { + "text": "验证 IDE 设置" + }, + "fullDescription": { + "text": "验证 IDE 设置名称和值。 Inspection ID: DevContainerIdeSettings", + "markdown": "验证 IDE 设置名称和值。\n\nInspection ID: DevContainerIdeSettings" + }, + "defaultConfiguration": { + "enabled": false, + "level": "error", + "parameters": { + "suppressToolId": "DevContainerIdeSettings", + "ideaSeverity": "ERROR", + "qodanaSeverity": "Critical", + "codeQualityCategory": "Unspecified" + } + }, + "relationships": [ + { + "target": { + "id": "Dev Container", + "index": 70, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "com.intellij.jsonpath", + "version": "261.26222.73", + "rules": [ + { + "id": "JsonPathEvaluateUnknownKey", + "shortDescription": { + "text": "为 JSONPath 求值表达式使用未知属性键" + }, + "fullDescription": { + "text": "报告源 JSON 文档中要求值的 JSONPath 表达式中缺少的键。 Inspection ID: JsonPathEvaluateUnknownKey", + "markdown": "报告源 JSON 文档中要求值的 JSONPath 表达式中缺少的键。\n\nInspection ID: JsonPathEvaluateUnknownKey" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JsonPathEvaluateUnknownKey", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JSONPath", + "index": 71, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JsonPathUnknownFunction", + "shortDescription": { + "text": "未知 JSONPath 函数" + }, + "fullDescription": { + "text": "报告在 JSONPath 函数调用中使用了未知名称,而不是已知的标准函数名称: 'concat'、'keys'、'length'、'min'、'max'、'avg'、'stddev'、'sum'。 Inspection ID: JsonPathUnknownFunction", + "markdown": "报告在 JSONPath 函数调用中使用了未知名称,而不是已知的标准函数名称: `concat`、`keys`、`length`、`min`、`max`、`avg`、`stddev`、`sum`。\n\nInspection ID: JsonPathUnknownFunction" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JsonPathUnknownFunction", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JSONPath", + "index": 71, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + }, + { + "id": "JsonPathUnknownOperator", + "shortDescription": { + "text": "未知 JSONPath 运算符" + }, + "fullDescription": { + "text": "报告在 JSONPath 表达式中使用了未知运算符,而不是其中一种标准运算符: 'in'、'nin'、'subsetof'、'anyof'、'noneof'、'size'、'empty'、'contains'。 Inspection ID: JsonPathUnknownOperator", + "markdown": "报告在 JSONPath 表达式中使用了未知运算符,而不是其中一种标准运算符: `in`、`nin`、`subsetof`、`anyof`、`noneof`、`size`、`empty`、`contains`。\n\nInspection ID: JsonPathUnknownOperator" + }, + "defaultConfiguration": { + "enabled": false, + "level": "warning", + "parameters": { + "suppressToolId": "JsonPathUnknownOperator", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Reliability" + } + }, + "relationships": [ + { + "target": { + "id": "JSONPath", + "index": 71, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + }, + { + "name": "org.toml.lang", + "version": "261.26222.73", + "rules": [ + { + "id": "TomlUnresolvedReference", + "shortDescription": { + "text": "未解析的引用" + }, + "fullDescription": { + "text": "报告 TOML 文件中未解析的引用。 Inspection ID: TomlUnresolvedReference", + "markdown": "报告 TOML 文件中未解析的引用。\n\nInspection ID: TomlUnresolvedReference" + }, + "defaultConfiguration": { + "enabled": true, + "level": "warning", + "parameters": { + "suppressToolId": "TomlUnresolvedReference", + "ideaSeverity": "WARNING", + "qodanaSeverity": "High", + "codeQualityCategory": "Sanity" + } + }, + "relationships": [ + { + "target": { + "id": "TOML", + "index": 75, + "toolComponent": { + "name": "RR" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "language": "en-US", + "contents": [ + "localizedData", + "nonLocalizedData" + ], + "isComprehensive": false + } + ] + }, + "invocations": [ + { + "startTimeUtc": "2026-07-16T00:56:42.253056Z", + "exitCode": 0, + "executionSuccessful": true + } + ], + "language": "en-US", + "versionControlProvenance": [ + { + "repositoryUri": "https://github.com/IvanHanloth/Boss-Key.git", + "revisionId": "a33c47131a3c7e1840727588c00f00c707ee52a7", + "branch": "chore/clean-up", + "properties": { + "repoUrl": "https://github.com/IvanHanloth/Boss-Key.git", + "lastAuthorName": "Ivan Hanloth", + "vcsType": "Git", + "lastAuthorEmail": "ivan@hanloth.com" + } + } + ], + "originalUriBaseIds": { + "SRCROOT": { + "description": { + "text": "The subdirectory within the project that was analyzed (--project-dir qodana CLI parameter)" + } + } + }, + "results": [ + { + "ruleId": "RsAssertEqual", + "kind": "fail", + "level": "note", + "message": { + "text": "`assert!(a != b)` 可被替换为 `assert_ne!(a, b)`", + "markdown": "\\`assert!(a != b)\\` 可被替换为 \\`assert_ne!(a, b)\\`" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/core/src/mouse_hook.rs", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 463, + "startColumn": 9, + "charOffset": 13667, + "charLength": 107, + "snippet": { + "text": "assert!(\r\n corner_flags_from(&Setting::default()) & F_FAST != 0,\r\n \"默认只认快速移动\"\r\n " + }, + "sourceLanguage": "Rust" + }, + "contextRegion": { + "startLine": 461, + "startColumn": 1, + "charOffset": 13590, + "charLength": 279, + "snippet": { + "text": " });\r\n assert_eq!(corner_flags_from(&s) & F_FAST, 0);\r\n assert!(\r\n corner_flags_from(&Setting::default()) & F_FAST != 0,\r\n \"默认只认快速移动\"\r\n );\r\n assert!(\r\n !wants_hook(&setting_with(|s| s.corner_fast_only = true)),\r" + }, + "sourceLanguage": "Rust" + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "Boss-Key", + "kind": "module" + } + ] + } + ], + "partialFingerprints": { + "equalIndicator/v2": "ca336fed2bd6a26a", + "equalIndicator/v1": "7ebabe6abc67f084ae94ca5f50edc7fe51e552ee5e2f3623c208f5dcf364d86a" + }, + "properties": { + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "problemType": "REGULAR", + "tags": [ + "Rust" + ] + } + }, + { + "ruleId": "RsUnnecessaryQualifications", + "kind": "fail", + "level": "note", + "message": { + "text": "路径前缀不必要", + "markdown": "路径前缀不必要" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/core/src/icon.rs", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 73, + "startColumn": 17, + "charOffset": 2271, + "charLength": 10, + "snippet": { + "text": "std::mem::" + }, + "sourceLanguage": "Rust" + }, + "contextRegion": { + "startLine": 71, + "startColumn": 1, + "charOffset": 2196, + "charLength": 196, + "snippet": { + "text": " if GetObjectW(\r\n color.into(),\r\n std::mem::size_of::() as i32,\r\n Some(&mut bmp as *mut BITMAP as *mut c_void),\r\n ) == 0\r" + }, + "sourceLanguage": "Rust" + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "Boss-Key", + "kind": "module" + } + ] + } + ], + "partialFingerprints": { + "equalIndicator/v2": "546ba0ca16e1947d", + "equalIndicator/v1": "0dceb28d2e83e49775668a590fab1bc4309a3e620ec198379b967aacc5496100" + }, + "properties": { + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "problemType": "REGULAR", + "tags": [ + "Rust" + ] + } + }, + { + "ruleId": "RsUnnecessaryQualifications", + "kind": "fail", + "level": "note", + "message": { + "text": "路径前缀不必要", + "markdown": "路径前缀不必要" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/core/src/freeze.rs", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 83, + "startColumn": 21, + "charOffset": 2744, + "charLength": 10, + "snippet": { + "text": "std::mem::" + }, + "sourceLanguage": "Rust" + }, + "contextRegion": { + "startLine": 81, + "startColumn": 1, + "charOffset": 2669, + "charLength": 168, + "snippet": { + "text": " };\r\n let mut entry = PROCESSENTRY32W {\r\n dwSize: std::mem::size_of::() as u32,\r\n ..Default::default()\r\n };\r" + }, + "sourceLanguage": "Rust" + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "Boss-Key", + "kind": "module" + } + ] + } + ], + "partialFingerprints": { + "equalIndicator/v2": "c9477bdf7d366cc8", + "equalIndicator/v1": "1a53aa74f109f6d7fcb4dbb71500cd44cbcfb79a89947eea695baad29083722f" + }, + "properties": { + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "problemType": "REGULAR", + "tags": [ + "Rust" + ] + } + }, + { + "ruleId": "RsUnnecessaryQualifications", + "kind": "fail", + "level": "note", + "message": { + "text": "路径前缀不必要", + "markdown": "路径前缀不必要" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/core/src/ipc_server.rs", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 194, + "startColumn": 13, + "charOffset": 5429, + "charLength": 10, + "snippet": { + "text": "std::mem::" + }, + "sourceLanguage": "Rust" + }, + "contextRegion": { + "startLine": 192, + "startColumn": 1, + "charOffset": 5360, + "charLength": 128, + "snippet": { + "text": " assert_eq!(\n sec.sa.nLength as usize,\n std::mem::size_of::()\n );\n }" + }, + "sourceLanguage": "Rust" + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "Boss-Key", + "kind": "module" + } + ] + } + ], + "partialFingerprints": { + "equalIndicator/v2": "97b037593971775b", + "equalIndicator/v1": "1f8c335225dac12c055aeb8ec81cf09bef3f1333873b940b68a76ab8a85d7581" + }, + "properties": { + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "problemType": "REGULAR", + "tags": [ + "Rust" + ] + } + }, + { + "ruleId": "RsUnnecessaryQualifications", + "kind": "fail", + "level": "note", + "message": { + "text": "路径前缀不必要", + "markdown": "路径前缀不必要" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/core/src/idle.rs", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 6, + "startColumn": 17, + "charOffset": 237, + "charLength": 10, + "snippet": { + "text": "std::mem::" + }, + "sourceLanguage": "Rust" + }, + "contextRegion": { + "startLine": 4, + "startColumn": 1, + "charOffset": 148, + "charLength": 159, + "snippet": { + "text": "pub fn idle_millis() -> Option {\n let mut info = LASTINPUTINFO {\n cbSize: std::mem::size_of::() as u32,\n dwTime: 0,\n };" + }, + "sourceLanguage": "Rust" + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "Boss-Key", + "kind": "module" + } + ] + } + ], + "partialFingerprints": { + "equalIndicator/v2": "2114c055f371ac92", + "equalIndicator/v1": "2d12c131b1b1fdbb675dce2d44c7e44cc08daa33db3ffc030cb586deaa87a19c" + }, + "properties": { + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "problemType": "REGULAR", + "tags": [ + "Rust" + ] + } + }, + { + "ruleId": "RsUnnecessaryQualifications", + "kind": "fail", + "level": "note", + "message": { + "text": "路径前缀不必要", + "markdown": "路径前缀不必要" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/core/src/tray.rs", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 107, + "startColumn": 21, + "charOffset": 2805, + "charLength": 10, + "snippet": { + "text": "std::mem::" + }, + "sourceLanguage": "Rust" + }, + "contextRegion": { + "startLine": 105, + "startColumn": 1, + "charOffset": 2697, + "charLength": 212, + "snippet": { + "text": " fn base_data(&self) -> NOTIFYICONDATAW {\r\n let mut data = NOTIFYICONDATAW {\r\n cbSize: std::mem::size_of::() as u32,\r\n hWnd: self.hwnd,\r\n uID: TRAY_ID,\r" + }, + "sourceLanguage": "Rust" + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "Boss-Key", + "kind": "module" + } + ] + } + ], + "partialFingerprints": { + "equalIndicator/v2": "4f2d5d20848b10c0", + "equalIndicator/v1": "58fc91dd5a5dd2c38a73de2560aa971718d85dd3862d2c5202127ee8dd7331b9" + }, + "properties": { + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "problemType": "REGULAR", + "tags": [ + "Rust" + ] + } + }, + { + "ruleId": "RsUnnecessaryQualifications", + "kind": "fail", + "level": "note", + "message": { + "text": "路径前缀不必要", + "markdown": "路径前缀不必要" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "apps/config/src-tauri/src/lib.rs", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 532, + "startColumn": 40, + "charOffset": 15663, + "charLength": 11, + "snippet": { + "text": "std::time::" + }, + "sourceLanguage": "Rust" + }, + "contextRegion": { + "startLine": 530, + "startColumn": 1, + "charOffset": 15510, + "charLength": 301, + "snippet": { + "text": " if let Some(window) = app.get_webview_window(\"main\") {\r\n std::thread::spawn(move || {\r\n std::thread::sleep(std::time::Duration::from_secs(5));\r\n if !window.is_visible().unwrap_or(false) {\r\n let _ = window.show();\r" + }, + "sourceLanguage": "Rust" + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "Boss-Key", + "kind": "module" + } + ] + } + ], + "partialFingerprints": { + "equalIndicator/v2": "b48f49f301e41b9d", + "equalIndicator/v1": "65e0ebb6ead46088b1ecb350f1590e85ba940c44c7de6854716b455e6b31482e" + }, + "properties": { + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "problemType": "REGULAR", + "tags": [ + "Rust" + ] + } + }, + { + "ruleId": "RsUnnecessaryQualifications", + "kind": "fail", + "level": "note", + "message": { + "text": "路径前缀不必要", + "markdown": "路径前缀不必要" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/core/src/icon.rs", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 87, + "startColumn": 25, + "charOffset": 2772, + "charLength": 10, + "snippet": { + "text": "std::mem::" + }, + "sourceLanguage": "Rust" + }, + "contextRegion": { + "startLine": 85, + "startColumn": 1, + "charOffset": 2667, + "charLength": 252, + "snippet": { + "text": " let hdc = GetDC(None);\r\n let header = BITMAPINFOHEADER {\r\n biSize: std::mem::size_of::() as u32,\r\n biWidth: width as i32,\r\n biHeight: -(height as i32), // 负值 = 顶到底行序\r" + }, + "sourceLanguage": "Rust" + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "Boss-Key", + "kind": "module" + } + ] + } + ], + "partialFingerprints": { + "equalIndicator/v2": "a4a4efb76d81d750", + "equalIndicator/v1": "699058fe2a76cc40ecdc0ad8b5aaf8093a5d42dd56f6a085d6ae2ee239da3545" + }, + "properties": { + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "problemType": "REGULAR", + "tags": [ + "Rust" + ] + } + }, + { + "ruleId": "RsUnnecessaryQualifications", + "kind": "fail", + "level": "note", + "message": { + "text": "路径前缀不必要", + "markdown": "路径前缀不必要" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/core/src/icon.rs", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 45, + "startColumn": 13, + "charOffset": 1477, + "charLength": 10, + "snippet": { + "text": "std::mem::" + }, + "sourceLanguage": "Rust" + }, + "contextRegion": { + "startLine": 43, + "startColumn": 1, + "charOffset": 1392, + "charLength": 182, + "snippet": { + "text": " FILE_FLAGS_AND_ATTRIBUTES(0),\r\n Some(&mut info),\r\n std::mem::size_of::() as u32,\r\n SHGFI_ICON | SHGFI_LARGEICON,\r\n )\r" + }, + "sourceLanguage": "Rust" + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "Boss-Key", + "kind": "module" + } + ] + } + ], + "partialFingerprints": { + "equalIndicator/v2": "744c16dfee540bfc", + "equalIndicator/v1": "86429ccdf282d3e34b2e78eba39096193af51101e488dcd07d923b798e2cfa8c" + }, + "properties": { + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "problemType": "REGULAR", + "tags": [ + "Rust" + ] + } + }, + { + "ruleId": "RsUnnecessaryQualifications", + "kind": "fail", + "level": "note", + "message": { + "text": "路径前缀不必要", + "markdown": "路径前缀不必要" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/core/src/ipc_server.rs", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 48, + "startColumn": 22, + "charOffset": 1491, + "charLength": 10, + "snippet": { + "text": "std::mem::" + }, + "sourceLanguage": "Rust" + }, + "contextRegion": { + "startLine": 46, + "startColumn": 1, + "charOffset": 1421, + "charLength": 203, + "snippet": { + "text": " }\n let sa = SECURITY_ATTRIBUTES {\n nLength: std::mem::size_of::() as u32,\n lpSecurityDescriptor: psd.0,\n bInheritHandle: false.into()," + }, + "sourceLanguage": "Rust" + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "Boss-Key", + "kind": "module" + } + ] + } + ], + "partialFingerprints": { + "equalIndicator/v2": "347129ca3c1c58f0", + "equalIndicator/v1": "c80a7eee5b601b62cc04bb998f33977d97ab39475d7698fea5605cc79b645a45" + }, + "properties": { + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "problemType": "REGULAR", + "tags": [ + "Rust" + ] + } + }, + { + "ruleId": "RsUnnecessaryQualifications", + "kind": "fail", + "level": "note", + "message": { + "text": "路径前缀不必要", + "markdown": "路径前缀不必要" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "crates/core/src/freeze.rs", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 113, + "startColumn": 21, + "charOffset": 3797, + "charLength": 10, + "snippet": { + "text": "std::mem::" + }, + "sourceLanguage": "Rust" + }, + "contextRegion": { + "startLine": 111, + "startColumn": 1, + "charOffset": 3722, + "charLength": 168, + "snippet": { + "text": " };\r\n let mut entry = PROCESSENTRY32W {\r\n dwSize: std::mem::size_of::() as u32,\r\n ..Default::default()\r\n };\r" + }, + "sourceLanguage": "Rust" + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "Boss-Key", + "kind": "module" + } + ] + } + ], + "partialFingerprints": { + "equalIndicator/v2": "7ec2fb43bfcd3147", + "equalIndicator/v1": "ff8de81b9ab41a9b62322eaa1af90282e5dc3c7a740e282166b9da628f31859e" + }, + "properties": { + "ideaSeverity": "WEAK WARNING", + "qodanaSeverity": "Moderate", + "problemType": "REGULAR", + "tags": [ + "Rust" + ] + } + } + ], + "automationDetails": { + "id": "Boss-Key/qodana/2026-07-16", + "guid": "3cfcd606-ad4b-4e80-8971-22f922906474", + "properties": { + "jobUrl": "", + "analysisKind": "ide" + } + }, + "newlineSequences": [ + "\r\n", + "\n" + ], + "properties": { + "qodana.coverage.files.provided": false, + "configProfile": "starter", + "deviceId": "0904241fc243541-8561-4533-9119-a208e32e0305", + "qodanaNewResultSummary": { + "moderate": 11.0, + "total": 11.0 + } + } + } + ], + "properties": { + "runTimestamp": "1784163447696" + } +} \ No newline at end of file diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..7f368e9 --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,46 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# + +################################################################################# +# WARNING: Do not store sensitive information in this file, # +# as its contents will be included in the Qodana report. # +################################################################################# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +# Quality gate. Will fail the CI/CD pipeline if any condition is not met +# severityThresholds - configures maximum thresholds for different problem severities +# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code +# Code Coverage is available in Ultimate and Ultimate Plus plans +#failureConditions: +# severityThresholds: +# any: 15 +# critical: 5 +# testCoverageThresholds: +# fresh: 70 +# total: 50 + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-:2026.1 From a2826ce2953620dfa9f1bf37827d70e8f67733e5 Mon Sep 17 00:00:00 2001 From: Ivan Hanloth Date: Sun, 26 Jul 2026 13:07:49 +0100 Subject: [PATCH 12/19] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0verhub=20sdk?= =?UTF-8?q?=E7=89=88=E6=9C=AC=EF=BC=8C=E4=BC=98=E5=8C=96=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 5 +- Cargo.toml | 2 +- apps/config/src-tauri/Cargo.toml | 3 + apps/config/src-tauri/src/lib.rs | 10 ++ apps/config/src-tauri/src/verhub.rs | 141 +++++++++++++++++- .../ui/src/components/AboutPanel.svelte | 20 ++- apps/config/ui/src/lib/ipc.js | 10 ++ apps/config/ui/src/lib/state.svelte.js | 7 + apps/config/ui/src/lib/verhub.js | 5 + docs/dev/architecture.md | 3 +- docs/dev/frontend.md | 2 +- docs/en/dev/architecture.md | 3 +- docs/en/dev/frontend.md | 2 +- docs/zh-tw/dev/architecture.md | 3 +- docs/zh-tw/dev/frontend.md | 2 +- 15 files changed, 200 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 962f2dc..3f2f960 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -308,6 +308,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-single-instance", + "tempfile", "verhub-sdk", ] @@ -4218,9 +4219,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "verhub-sdk" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61704ff45c9dd4e311e21b254be2405e8e3c9a81a5c1ad0ef8e8bb123fe65115" +checksum = "b6d327c305b7a92625c9bd30009a1ab80091c401c268b52c4d4576978aecb165" dependencies = [ "log", "os_info", diff --git a/Cargo.toml b/Cargo.toml index cdfa8d3..112262b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ regex = "1.11.1" tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros", "net", "io-util", "sync", "time"] } windows = "0.62.2" tempfile = "3.27.0" -verhub-sdk = { version = "0.2.5", default-features = false, features = ["native-tls"] } +verhub-sdk = { version = "0.2.6", default-features = false, features = ["native-tls"] } [profile.release] opt-level = "z" diff --git a/apps/config/src-tauri/Cargo.toml b/apps/config/src-tauri/Cargo.toml index 6069384..2962491 100644 --- a/apps/config/src-tauri/Cargo.toml +++ b/apps/config/src-tauri/Cargo.toml @@ -21,3 +21,6 @@ bosskey-common = { path = "../../../crates/common" } bosskey-core = { path = "../../../crates/core" } tauri-plugin-single-instance = "2.4.2" verhub-sdk = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } diff --git a/apps/config/src-tauri/src/lib.rs b/apps/config/src-tauri/src/lib.rs index 748491d..8e775a3 100644 --- a/apps/config/src-tauri/src/lib.rs +++ b/apps/config/src-tauri/src/lib.rs @@ -402,6 +402,15 @@ async fn open_external(url: String) -> Result<(), String> { blocking(move || bosskey_core::shell::open(&url)).await } +/// 项目公开链接(主页 / 仓库 / 文档等)。带缓存(内存 + exe 同目录磁盘文件, +/// 有效期一天),过期才请求 Verhub;请求失败退回过期缓存。 +#[tauri::command] +async fn verhub_project_links() -> Result { + verhub::project_links(&exe_dir().join("verhub_cache.json")) + .await + .map_err(|e| e.to_string()) +} + /// 检查更新。`required=true` 即强制更新,界面须阻断使用。 #[tauri::command] async fn verhub_check_update(include_preview: bool) -> Result { @@ -528,6 +537,7 @@ pub fn run() { startup_action, app_info, open_external, + verhub_project_links, verhub_check_update, verhub_announcements, verhub_submit_feedback, diff --git a/apps/config/src-tauri/src/verhub.rs b/apps/config/src-tauri/src/verhub.rs index 5f8e9b2..f9998e7 100644 --- a/apps/config/src-tauri/src/verhub.rs +++ b/apps/config/src-tauri/src/verhub.rs @@ -1,15 +1,17 @@ -//! Verhub 客户端:版本 / 公告 / 反馈 / 日志,基于官方 verhub-sdk。 +//! Verhub 客户端:版本 / 公告 / 反馈 / 日志 / 项目链接,基于官方 verhub-sdk。 //! //! 只用公开端点(无需凭据)。HTTP 由 SDK 完成;本模块把 SDK 的响应类型映射成 //! 前端 IPC 契约所需的可序列化 DTO,字段名保持不变。 +use std::path::Path; +use std::sync::Mutex; use std::time::Duration; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use verhub_sdk::VerhubClient; use verhub_sdk::models::{ AnnouncementItem, CheckUpdateInput, CreateFeedbackInput, JsonObject, ListAnnouncementsOptions, - LogLevel, Platform, UploadLogInput, VersionDownloadLink, VersionItem, + LogLevel, Platform, ProjectItem, UploadLogInput, VersionDownloadLink, VersionItem, }; /// Verhub 基础路径。 @@ -190,6 +192,101 @@ pub async fn upload_log(content: &str, device_info: serde_json::Value) -> Result Ok(()) } +/// 项目公开链接(主页 / 仓库 / 文档等)的缓存有效期。链接极少变动,一天刷新一次足够。 +const PROJECT_CACHE_TTL_SECS: i64 = 24 * 60 * 60; + +/// 项目公开链接。所有字段都可能缺省(Verhub 上未填写);前端须自备回退链接。 +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ProjectLinks { + pub name: Option, + pub website_url: Option, + pub repo_url: Option, + pub docs_url: Option, + pub author: Option, + pub author_homepage_url: Option, + /// 拉取时刻(Unix 秒),用于判断缓存新鲜度。 + pub fetched_at: i64, +} + +/// 进程内缓存,避免每次都读盘。 +static PROJECT_CACHE: Mutex> = Mutex::new(None); + +fn map_project(item: ProjectItem, fetched_at: i64) -> ProjectLinks { + ProjectLinks { + name: Some(item.name), + website_url: item.website_url, + repo_url: item.repo_url, + docs_url: item.docs_url, + author: item.author, + author_homepage_url: item.author_homepage_url, + fetched_at, + } +} + +/// 缓存是否仍在有效期内。`fetched_at` 在未来(时钟回拨)按过期处理。 +fn cache_fresh(links: &ProjectLinks, now: i64) -> bool { + (0..PROJECT_CACHE_TTL_SECS).contains(&(now - links.fetched_at)) +} + +fn cache_get() -> Option { + PROJECT_CACHE.lock().ok()?.clone() +} + +fn cache_put(links: ProjectLinks) { + if let Ok(mut cache) = PROJECT_CACHE.lock() { + *cache = Some(links); + } +} + +/// 读磁盘缓存;文件不存在或损坏一律当作没有缓存。 +fn read_cache_file(path: &Path) -> Option { + let content = std::fs::read_to_string(path).ok()?; + serde_json::from_str(&content).ok() +} + +/// 写磁盘缓存;尽力而为,写失败只影响下次冷启动的命中率。 +fn write_cache_file(path: &Path, links: &ProjectLinks) { + if let Ok(json) = serde_json::to_string_pretty(links) { + let _ = std::fs::write(path, json); + } +} + +fn unix_now() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0) +} + +/// 项目公开链接:内存缓存 → 磁盘缓存(`cache_path`)→ Verhub API 逐级回退。 +/// API 拉取失败时退回过期缓存(有旧数据总比没有强),完全没有缓存才报错。 +pub async fn project_links(cache_path: &Path) -> Result { + let now = unix_now(); + if let Some(cached) = cache_get() + && cache_fresh(&cached, now) + { + return Ok(cached); + } + if let Some(cached) = read_cache_file(cache_path) + && cache_fresh(&cached, now) + { + cache_put(cached.clone()); + return Ok(cached); + } + match client()?.public().get_project().await { + Ok(item) => { + let links = map_project(item, now); + write_cache_file(cache_path, &links); + cache_put(links.clone()); + Ok(links) + } + Err(err) => match cache_get().or_else(|| read_cache_file(cache_path)) { + Some(stale) => Ok(stale), + None => Err(err), + }, + } +} + /// 截到上限以内,按字符边界切以避免切碎多字节字符。 fn truncate_log(content: &str) -> String { if content.len() <= LOG_CONTENT_MAX { @@ -232,4 +329,42 @@ mod tests { assert_eq!(u8::from(LogLevel::Debug), 0); assert_eq!(u8::from(LogLevel::Error), 3); } + + #[test] + fn cache_fresh_within_ttl_only() { + let links = ProjectLinks { + fetched_at: 1_000_000, + ..Default::default() + }; + assert!(cache_fresh(&links, 1_000_000)); // 刚拉取 + assert!(cache_fresh(&links, 1_000_000 + PROJECT_CACHE_TTL_SECS - 1)); + assert!(!cache_fresh(&links, 1_000_000 + PROJECT_CACHE_TTL_SECS)); // 到期 + assert!(!cache_fresh(&links, 999_999)); // 时钟回拨 + } + + #[test] + fn cache_file_roundtrip() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("verhub_cache.json"); + let links = ProjectLinks { + name: Some("Boss Key".into()), + website_url: Some("https://example.com/".into()), + fetched_at: 42, + ..Default::default() + }; + write_cache_file(&path, &links); + let read = read_cache_file(&path).expect("缓存应可读回"); + assert_eq!(read.name.as_deref(), Some("Boss Key")); + assert_eq!(read.website_url.as_deref(), Some("https://example.com/")); + assert_eq!(read.fetched_at, 42); + } + + #[test] + fn cache_file_tolerates_missing_and_corrupt() { + let dir = tempfile::tempdir().unwrap(); + assert!(read_cache_file(&dir.path().join("absent.json")).is_none()); + let bad = dir.path().join("bad.json"); + std::fs::write(&bad, "not json{{").unwrap(); + assert!(read_cache_file(&bad).is_none()); + } } diff --git a/apps/config/ui/src/components/AboutPanel.svelte b/apps/config/ui/src/components/AboutPanel.svelte index cf12f5c..9631375 100644 --- a/apps/config/ui/src/components/AboutPanel.svelte +++ b/apps/config/ui/src/components/AboutPanel.svelte @@ -24,6 +24,14 @@ const v = $derived(app.config.verhub); const year = new Date().getFullYear(); + // 链接以 Verhub 项目信息为准(后端带缓存),拉不到时退回内置地址。 + const links = $derived(app.project); + const homepageUrl = $derived(links?.website_url || "https://boss-key.ivan-hanloth.cn/"); + const repoUrl = $derived(links?.repo_url || info?.website || "https://github.com/IvanHanloth/Boss-Key"); + const docsUrl = $derived(links?.docs_url || "https://boss-key.ivan-hanloth.cn/guide/"); + const authorUrl = $derived(links?.author_homepage_url || info?.blog || "https://www.ivan-hanloth.cn/"); + const authorName = $derived(links?.author || info?.author || "Ivan Hanloth"); + let content = $state(""); let rating = $state(0); let hoverRating = $state(0); @@ -81,22 +89,22 @@

        {t("about.version", { version: info?.version ?? "…" })}

        {t("about.tagline")}

        - - - - +

        Copyright © 2022-{year} - All Rights Reserved.

        diff --git a/apps/config/ui/src/lib/ipc.js b/apps/config/ui/src/lib/ipc.js index 55d4ea3..7cae640 100644 --- a/apps/config/ui/src/lib/ipc.js +++ b/apps/config/ui/src/lib/ipc.js @@ -152,6 +152,16 @@ function mockInvoke(cmd, args) { blog: "https://blog.ivan-hanloth.cn/", license: "MIT", }; + case "verhub_project_links": + return { + name: "Boss Key", + website_url: "https://boss-key.ivan-hanloth.cn/", + repo_url: "https://github.com/IvanHanloth/Boss-Key", + docs_url: "https://boss-key.ivan-hanloth.cn/guide/", + author: "Ivan Hanloth", + author_homepage_url: "https://www.ivan-hanloth.cn/", + fetched_at: Math.floor(Date.now() / 1000), + }; case "verhub_check_update": return { should_update: false, diff --git a/apps/config/ui/src/lib/state.svelte.js b/apps/config/ui/src/lib/state.svelte.js index 9038ebd..5e272f9 100644 --- a/apps/config/ui/src/lib/state.svelte.js +++ b/apps/config/ui/src/lib/state.svelte.js @@ -23,6 +23,8 @@ export const app = $state({ /** 当前自启注册方式:"task"|"registry"|null(未注册)。 */ autostartMethod: null, info: null, + /** Verhub 上的项目公开链接(主页 / 仓库 / 文档等);null 时用内置回退链接。 */ + project: null, maximized: false, saving: false, /** 程序目录下是否存在 pssuspend64.exe(增强冻结的前置条件)。 */ @@ -139,6 +141,11 @@ export async function loadAll() { }), invoke("pssuspend_available").then((v) => (app.pssuspend = !!v)), ]; + // 项目链接拉不到时静默——「关于」页有内置回退链接,不值得打扰用户。 + verhub + .projectLinks() + .then((p) => (app.project = p)) + .catch(() => {}); const results = await Promise.allSettled(tasks); const failed = results.find((r) => r.status === "rejected"); if (failed) toast(t("state.partialLoadFailed", { reason: failed.reason }), true); diff --git a/apps/config/ui/src/lib/verhub.js b/apps/config/ui/src/lib/verhub.js index 743caed..2c04068 100644 --- a/apps/config/ui/src/lib/verhub.js +++ b/apps/config/ui/src/lib/verhub.js @@ -2,6 +2,11 @@ import { invoke } from "./ipc.js"; +/** 项目公开链接(主页 / 仓库 / 文档等)。后端带缓存,可随意调用。 */ +export function projectLinks() { + return invoke("verhub_project_links"); +} + /** 检查更新。返回 { should_update, required, target_version, latest_version, … }。 */ export function checkUpdate(includePreview = false) { return invoke("verhub_check_update", { includePreview }); diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index 51f54c6..9d9e3e1 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -97,7 +97,8 @@ Boss-Key/ │ single_instance.rs 命名互斥单实例 └── apps/config/ 配置界面(Tauri 2 + Svelte 5) ├── src-tauri/ Rust 后端命令 + tauri.conf.json + capabilities - │ └── src/verhub.rs Verhub 客户端(版本/公告/反馈/日志,基于 verhub-sdk) + │ └── src/verhub.rs Verhub 客户端(版本/公告/反馈/日志/项目链接,基于 verhub-sdk; + │ 项目链接带缓存:内存 + 同目录 verhub_cache.json,有效期一天) ├── ui/ 前端源码(Vite + Svelte 5) │ └── src/ lib/(纯逻辑 + vitest 测试)+ components/(Svelte 组件) │ + locales/(三语文案 catalog,以 zh-CN.js 为基准) diff --git a/docs/dev/frontend.md b/docs/dev/frontend.md index 619435e..7e2755d 100644 --- a/docs/dev/frontend.md +++ b/docs/dev/frontend.md @@ -31,7 +31,7 @@ apps/config/ui/ │ ├── theme.js 主题切换(配 theme.test.js) │ ├── i18n.svelte.js 界面语言:catalog 查表 + 语言解析(配 i18n.test.js) │ ├── markdown.js 公告/更新日志的 Markdown 渲染(配 markdown.test.js) -│ └── verhub.js 检查更新/公告/打开外链 +│ └── verhub.js 检查更新/公告/项目链接/打开外链 ├── locales/ 三语文案 catalog(zh-CN.js / en.js / zh-TW.js) ├── vite.config.js └── svelte.config.js diff --git a/docs/en/dev/architecture.md b/docs/en/dev/architecture.md index d1e4df7..461fd7a 100644 --- a/docs/en/dev/architecture.md +++ b/docs/en/dev/architecture.md @@ -97,7 +97,8 @@ Boss-Key/ │ single_instance.rs Named-mutex single instance └── apps/config/ Settings window (Tauri 2 + Svelte 5) ├── src-tauri/ Rust backend commands + tauri.conf.json + capabilities - │ └── src/verhub.rs Verhub client (versions/announcements/feedback/logs, built on verhub-sdk) + │ └── src/verhub.rs Verhub client (versions/announcements/feedback/logs/project links, built on verhub-sdk; + │ project links are cached: in memory + verhub_cache.json next to the exe, valid for one day) ├── ui/ Frontend source (Vite + Svelte 5) │ └── src/ lib/ (pure logic + vitest tests) + components/ (Svelte components) │ + locales/ (three-language catalogs; zh-CN.js is the source of truth) diff --git a/docs/en/dev/frontend.md b/docs/en/dev/frontend.md index 77597af..28a7d84 100644 --- a/docs/en/dev/frontend.md +++ b/docs/en/dev/frontend.md @@ -31,7 +31,7 @@ apps/config/ui/ │ ├── theme.js Theme switching (with theme.test.js) │ ├── i18n.svelte.js Display language: catalog lookup + language resolution (with i18n.test.js) │ ├── markdown.js Markdown rendering for announcements/release notes (with markdown.test.js) -│ └── verhub.js Update checks / announcements / opening external links +│ └── verhub.js Update checks / announcements / project links / opening external links ├── locales/ Three-language catalogs (zh-CN.js / en.js / zh-TW.js) ├── vite.config.js └── svelte.config.js diff --git a/docs/zh-tw/dev/architecture.md b/docs/zh-tw/dev/architecture.md index 3657e24..c929166 100644 --- a/docs/zh-tw/dev/architecture.md +++ b/docs/zh-tw/dev/architecture.md @@ -97,7 +97,8 @@ Boss-Key/ │ single_instance.rs 具名互斥鎖單一執行個體 └── apps/config/ 設定介面(Tauri 2 + Svelte 5) ├── src-tauri/ Rust 後端命令 + tauri.conf.json + capabilities - │ └── src/verhub.rs Verhub 用戶端(版本/公告/回饋/日誌,基於 verhub-sdk) + │ └── src/verhub.rs Verhub 用戶端(版本/公告/回饋/日誌/專案連結,基於 verhub-sdk; + │ 專案連結帶快取:記憶體 + 同目錄 verhub_cache.json,有效期一天) ├── ui/ 前端原始碼(Vite + Svelte 5) │ └── src/ lib/(純邏輯 + vitest 測試)+ components/(Svelte 元件) │ + locales/(三語文案 catalog,以 zh-CN.js 為基準) diff --git a/docs/zh-tw/dev/frontend.md b/docs/zh-tw/dev/frontend.md index c807bd9..790a6ee 100644 --- a/docs/zh-tw/dev/frontend.md +++ b/docs/zh-tw/dev/frontend.md @@ -31,7 +31,7 @@ apps/config/ui/ │ ├── theme.js 佈景主題切換(配 theme.test.js) │ ├── i18n.svelte.js 介面語言:catalog 查表 + 語言解析(配 i18n.test.js) │ ├── markdown.js 公告/更新記錄的 Markdown 算繪(配 markdown.test.js) -│ └── verhub.js 檢查更新/公告/開啟外部連結 +│ └── verhub.js 檢查更新/公告/專案連結/開啟外部連結 ├── locales/ 三語文案 catalog(zh-CN.js / en.js / zh-TW.js) ├── vite.config.js └── svelte.config.js From 12976d9d21e0f6809d5e6e474bc66a748f8e5534 Mon Sep 17 00:00:00 2001 From: Ivan Hanloth Date: Sun, 26 Jul 2026 14:15:42 +0100 Subject: [PATCH 13/19] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=AD=98=E5=82=A8=E3=80=81=E6=97=A5=E5=BF=97=E8=BE=93?= =?UTF-8?q?=E5=87=BA=EF=BC=8C=E7=8B=AC=E7=AB=8B=E5=89=AF=E4=BD=9C=E7=94=A8?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/core/src/agent.rs | 279 ++++++------ crates/core/src/effects.rs | 43 +- crates/core/src/effects_worker.rs | 159 +++++++ crates/core/src/hide.rs | 654 ++++++++++++++++++++++------ crates/core/src/i18n.rs | 7 +- crates/core/src/lib.rs | 1 + crates/core/src/logging.rs | 37 ++ crates/core/src/platform/mod.rs | 6 + crates/core/src/platform/win32.rs | 105 ++++- crates/core/src/recovery.rs | 159 ++++++- crates/core/tests/agent_recovery.rs | 64 ++- docs/dev/architecture.md | 13 +- docs/en/dev/architecture.md | 13 +- docs/zh-tw/dev/architecture.md | 13 +- 14 files changed, 1242 insertions(+), 311 deletions(-) create mode 100644 crates/core/src/effects_worker.rs diff --git a/crates/core/src/agent.rs b/crates/core/src/agent.rs index 7b379df..d92b0f8 100644 --- a/crates/core/src/agent.rs +++ b/crates/core/src/agent.rs @@ -21,10 +21,11 @@ use windows::Win32::UI::WindowsAndMessaging::{ use windows::core::{PCWSTR, w}; use crate::effects::WinEffects; +use crate::effects_worker::{AsyncEffects, EffectsWorker}; use crate::float_window::{FLOAT_MENU, FLOAT_TOGGLE, FloatWindow, WM_APP_FLOAT}; use crate::hide::{ - HideController, RuleOutcome, expand_descendants, foreground_target, freezable_pids, - resolve_targets, + HideController, HidePlan, RuleOutcome, ShowOutcome, expand_descendants, foreground_target, + freezable_pids, resolve_targets, }; use crate::hotkey::{MOD_NOREPEAT, ParsedHotkey, is_disabled, parse_hotkey}; use crate::i18n::{self, Msg}; @@ -34,7 +35,7 @@ use crate::platform::win32::WindowsWindowManager; use crate::tray::TrayIcon; use crate::tray_badge::TrayIconSet; use crate::util::append_menu_item; -use crate::{idle, ipc_server, logging, recovery}; +use crate::{idle, ipc_server, log_error, log_warn, logging, recovery}; const HK_HIDE: i32 = 1; const HK_CLOSE: i32 = 2; @@ -62,6 +63,8 @@ const TRAY_RETRY_TIMER_ID: usize = 13; const AUTO_HIDE_INTERVAL_MS: u32 = 5000; const TRAY_RETRY_INTERVAL_MS: u32 = 2000; const IPC_REPLY_TIMEOUT: Duration = Duration::from_secs(3); +/// 退出时等待副作用线程排干队列(解冻 / 取消静音)的上限。 +const EFFECTS_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10); pub struct AgentOptions { pub config_path: PathBuf, @@ -85,7 +88,11 @@ struct AgentState { config: Config, config_path: PathBuf, recovery_path: PathBuf, - controller: HideController, + controller: HideController, + /// 副作用专职线程;退出时须 shutdown 排干队列。 + effects_worker: Option, + /// 恢复文件写入失败是否已提醒过(每次运行只弹一次气泡)。 + persist_warned: bool, tray: Option, /// 托盘图标的角标变体缓存;未启用托盘时为 None。 tray_icons: Option, @@ -136,18 +143,18 @@ impl AgentState { ), ] { if is_disabled(raw) { - logging::info(&format!("{label}热键已置空,不注册")); + logging::debug(&format!("{label}热键已置空,不注册")); continue; } match parse_hotkey(raw) { Ok(hk) if intercept => intercepts.push((id, label, raw.clone(), hk)), Ok(hk) => unsafe { match register(hwnd, id, &hk) { - Ok(()) => logging::info(&format!("{label}热键已注册: {raw}")), - Err(e) => logging::warn(&hotkey_failure_message(label, raw, &e)), + Ok(()) => logging::debug(&format!("{label}热键已注册: {raw}")), + Err(e) => log_warn!("{}", hotkey_failure_message(label, raw, &e)), } }, - Err(e) => logging::warn(&format!("{label}热键解析失败,该热键不生效: {raw} — {e}")), + Err(e) => log_warn!("{label}热键解析失败,该热键不生效: {raw} — {e}"), } } self.arm_intercepts(hwnd, intercepts); @@ -174,17 +181,17 @@ impl AgentState { } if self.keyboard_hook.is_some() { for (_, label, raw, _) in &intercepts { - logging::info(&format!("{label}热键已由键盘钩子拦截(不传递): {raw}")); + logging::debug(&format!("{label}热键已由键盘钩子拦截(不传递): {raw}")); } return; } keyboard_hook::set_hotkeys(&[]); - logging::warn("键盘钩子安装失败,「不传递」不生效,相关热键回退为普通注册"); + log_warn!("键盘钩子安装失败,「不传递」不生效,相关热键回退为普通注册"); for (id, label, raw, hk) in &intercepts { unsafe { match register(hwnd, *id, hk) { - Ok(()) => logging::info(&format!("{label}热键已注册: {raw}")), - Err(e) => logging::warn(&hotkey_failure_message(label, raw, &e)), + Ok(()) => logging::debug(&format!("{label}热键已注册: {raw}")), + Err(e) => log_warn!("{}", hotkey_failure_message(label, raw, &e)), } } } @@ -283,89 +290,78 @@ impl AgentState { }); } - /// 隐藏状态落盘:崩溃后下次启动据此找回窗口。快照为空时清除文件。 - fn persist_recovery(&self) { - if let Err(e) = recovery::save(&self.recovery_path, &self.controller.snapshot()) { - // 落盘失败不影响本次隐藏,但核心若随后崩溃,这些窗口将无法自动找回。 - logging::warn(&format!( + /// 把指定快照落盘。失败不阻断本次隐藏,只影响崩溃找回;每次运行提醒用户一次。 + fn persist_snapshot(&mut self, snapshot: &recovery::Snapshot) { + if let Err(e) = recovery::save(&self.recovery_path, snapshot) { + log_error!( "写入崩溃恢复文件失败,核心若异常退出将无法自动找回窗口: {} — {e}", self.recovery_path.display() - )); + ); + if !self.persist_warned { + self.persist_warned = true; + self.balloon(APP_NAME, i18n::t(Msg::RecoveryPersistFailedBody)); + } } } - fn apply_hide(&mut self) { - let windows = self.controller.enumerate(); - let foreground = self.controller.foreground(); - let (targets, outcomes) = resolve_targets(&mut self.config, &windows, foreground); - let freezable = freezable_pids(&targets, &windows); - // 「冻结完整进程」:把可冻结集展开到整棵子进程树。 - let freeze_set = if self.config.setting.freeze_whole_tree { + /// 按控制器当前状态落盘。 + fn persist_recovery(&mut self) { + let snapshot = self.controller.snapshot(); + self.persist_snapshot(&snapshot); + } + + /// 意图先行:先落盘计划后的快照,再隐藏窗口;副作用由专职线程异步执行。 + fn hide_with_plan(&mut self, targets: &[crate::hide::Target], freeze_set: &[u32]) -> HidePlan { + let plan = self + .controller + .plan_hide(&self.config.setting, targets, freeze_set); + let planned = self.controller.planned_snapshot(&plan); + self.persist_snapshot(&planned); + self.controller.commit_hide(plan.clone()); + self.sync_tray(); + if self.config.notifications.on_hide && !plan.fresh.is_empty() { + self.balloon(APP_NAME, i18n::t(Msg::HiddenBody)); + } + plan + } + + /// 「冻结完整进程」开启时把可冻结集展开到整棵子进程树。 + fn freeze_set(&self, freezable: Vec) -> Vec { + if self.config.setting.freeze_whole_tree { expand_descendants(&freezable, &crate::freeze::process_tree()) } else { freezable - }; - log_resolution(&self.config, &outcomes, &targets, &windows); - // 冻结开启时,把最终冻结集连同进程名逐个写入日志,便于定位冻结问题。 - if self.config.setting.freeze_after_hide && !freeze_set.is_empty() { - let names = crate::freeze::process_names(); - logging::info(&format!( - "本次冻结集共 {} 个进程(增强冻结={},完整进程树={})", - freeze_set.len(), - self.config.setting.enhanced_freeze, - self.config.setting.freeze_whole_tree - )); - for pid in &freeze_set { - let name = names.get(pid).map(String::as_str).unwrap_or("未知进程"); - logging::info(&format!("待冻结进程 pid={pid} name={name}")); - } - } - self.controller - .apply_hide(&self.config.setting, &targets, &freeze_set); - self.persist_recovery(); - self.sync_tray(); - if self.config.notifications.on_hide { - self.balloon(APP_NAME, i18n::t(Msg::HiddenBody)); } } + + fn apply_hide(&mut self) { + let windows = self.controller.enumerate(); + let foreground = self.controller.foreground(); + let (targets, outcomes) = resolve_targets(&mut self.config, &windows, foreground); + let freeze_set = self.freeze_set(freezable_pids(&targets, &windows)); + let plan = self.hide_with_plan(&targets, &freeze_set); + log_hide(&self.config, &outcomes, &plan); + } + fn apply_hide_foreground(&mut self) { let windows = self.controller.enumerate(); let foreground = self.controller.foreground(); let Some(target) = foreground_target(&windows, foreground) else { - logging::info("触发隐藏前台窗口:当前没有可隐藏的前台窗口,本次不隐藏"); + logging::info("隐藏前台窗口:当前没有可隐藏的前台窗口"); return; }; let targets = [target]; - let freezable = freezable_pids(&targets, &windows); - let freeze_set = if self.config.setting.freeze_whole_tree { - expand_descendants(&freezable, &crate::freeze::process_tree()) - } else { - freezable - }; - - let w = windows.iter().find(|w| w.hwnd == target.hwnd); - logging::info(&format!( - "触发隐藏前台窗口 hwnd={} pid={} process={} title=「{}」", - target.hwnd, - target.pid, - w.map(|w| w.process.as_str()).unwrap_or("未知进程"), - w.map(|w| w.title.as_str()).unwrap_or(""), - )); - - self.controller - .apply_hide(&self.config.setting, &targets, &freeze_set); - self.persist_recovery(); - self.sync_tray(); - if self.config.notifications.on_hide { - self.balloon(APP_NAME, i18n::t(Msg::HiddenBody)); + let freeze_set = self.freeze_set(freezable_pids(&targets, &windows)); + let plan = self.hide_with_plan(&targets, &freeze_set); + if let Some(t) = plan.fresh.first() { + logging::info(&format!("隐藏前台窗口: {}", t.describe())); } } fn apply_show(&mut self) { - let count = self.controller.hidden_count(); - self.controller.show(); - logging::info(&format!("已恢复显示 {count} 个窗口")); + let outcome = self.controller.show(); + log_show(outcome); self.persist_recovery(); self.sync_tray(); if self.config.notifications.on_show { @@ -473,23 +469,12 @@ impl AgentState { } } -/// 记录本次隐藏的分级日志。 -fn log_resolution( - config: &Config, - outcomes: &[RuleOutcome], - targets: &[crate::hide::Target], - windows: &[bosskey_common::WindowInfo], -) { - logging::info(&format!( - "触发隐藏:命中 {} 个窗口(窗口规则 {} 条 / 进程规则 {} 条)", - targets.len(), - config.window_rules.len(), - config.process_rules.len() - )); +/// 摘要式记录本次隐藏;规则失效 / 追溯事件逐条保留。 +fn log_hide(config: &Config, outcomes: &[RuleOutcome], plan: &HidePlan) { for (rule, outcome) in config.window_rules.iter().zip(outcomes) { match outcome { RuleOutcome::Reacquired => logging::info(&format!( - "窗口规则「{}」的句柄已失效(目标程序重启过),已重新匹配到当前窗口并更新规则", + "窗口规则「{}」的句柄已失效(目标程序重启过),已重新匹配并更新规则", rule.title )), // 「未能追溯」只说明当前没有匹配的窗口,看不出是关闭了还是标题变了,故不臆断原因。 @@ -500,18 +485,48 @@ fn log_resolution( _ => {} } } - // 逐个写出隐藏目标的句柄、PID 与进程名/标题(info 级,release 也可见)。 - for t in targets { - let w = windows.iter().find(|w| w.hwnd == t.hwnd); - let process = w.map(|w| w.process.as_str()).unwrap_or("未知进程"); - let title = w.map(|w| w.title.as_str()).unwrap_or(""); + if plan.fresh.is_empty() { + logging::info("触发隐藏:没有新的目标窗口"); + return; + } + logging::info(&format!( + "隐藏 {} 个窗口: {}", + plan.fresh.len(), + summarize(plan.fresh.iter().map(|t| t.describe())) + )); + if !plan.freeze.is_empty() { logging::info(&format!( - "隐藏目标 hwnd={} pid={} process={process} title=「{title}」", - t.hwnd, t.pid + "冻结 {} 个进程(增强={}): {}", + plan.freeze.len(), + plan.enhanced, + summarize(plan.freeze.iter().map(|r| r.pid.to_string())) )); } } +/// 记录恢复结果;有失效记录被跳过时如实写出。 +fn log_show(outcome: ShowOutcome) { + if outcome.stale > 0 { + logging::info(&format!( + "恢复显示 {} 个窗口,另有 {} 条记录已失效(窗口已关闭或句柄被复用),已跳过", + outcome.shown, outcome.stale + )); + } else { + logging::info(&format!("恢复显示 {} 个窗口", outcome.shown)); + } +} + +/// 拼接清单,最多列出前 8 项,其余以「等 N 项」收尾。 +fn summarize(items: impl Iterator) -> String { + const MAX: usize = 8; + let all: Vec = items.collect(); + if all.len() <= MAX { + all.join("、") + } else { + format!("{}、等 {} 项", all[..MAX].join("、"), all.len()) + } +} + unsafe fn register(hwnd: HWND, id: i32, hk: &ParsedHotkey) -> windows::core::Result<()> { unsafe { RegisterHotKey( @@ -571,9 +586,7 @@ fn toggle_auto_hide(state: &mut AgentState, hwnd: HWND) { "托盘菜单:已暂停自动隐藏" }); if let Err(e) = state.config.save(&state.config_path) { - logging::warn(&format!( - "自动隐藏开关写入配置失败,核心重启后将丢失本次切换: {e}" - )); + log_warn!("自动隐藏开关写入配置失败,核心重启后将丢失本次切换: {e}"); } // 重新武装/停掉空闲检测定时器,并刷新托盘角标。 state.refresh_runtime(hwnd); @@ -727,14 +740,21 @@ fn launch_settings(state: &mut AgentState, action: Option<&str>) { cmd.arg(arg); } if let Err(e) = cmd.spawn() { - logging::warn(&format!("启动配置程序失败: {e}")); + log_warn!("启动配置程序失败: {e}"); } } fn quit(state: &mut AgentState, hwnd: HWND) { - state.controller.show(); + let outcome = state.controller.show(); + if outcome.shown > 0 || outcome.stale > 0 { + log_show(outcome); + } state.persist_recovery(); state.unregister_hotkeys(hwnd); + // 排干副作用队列,确保退出前解冻 / 取消静音已生效。 + if let Some(worker) = state.effects_worker.take() { + worker.shutdown(EFFECTS_SHUTDOWN_TIMEOUT); + } logging::info("核心正常退出"); let notify_quit = state.config.notifications.on_quit; if let Some(tray) = &mut state.tray { @@ -941,17 +961,14 @@ fn load_config_logging_fallback(path: &Path) -> Config { match Config::load_reporting(path) { Ok((config, None)) => config, Ok((config, Some(parse_error))) => { - logging::error(&format!( + log_error!( "配置文件解析失败,{FALLBACK}: {} — {parse_error}", path.display() - )); + ); config } Err(e) => { - logging::error(&format!( - "配置文件读取失败,{FALLBACK}: {} — {e}", - path.display() - )); + log_error!("配置文件读取失败,{FALLBACK}: {} — {e}", path.display()); Config::default() } } @@ -975,14 +992,14 @@ pub fn run(options: AgentOptions) { // 记录当前版本并落盘,避免下次启动重复弹出(首次启动时顺带创建配置文件)。 config.version = bosskey_common::APP_CONFIG_VERSION.to_string(); if let Err(e) = config.save(&options.config_path) { - logging::warn(&format!("写入配置版本失败: {e}")); + log_warn!("写入配置版本失败: {e}"); } if let Some(path) = find_config_exe() { if let Err(e) = std::process::Command::new(&path).spawn() { - logging::warn(&format!("启动配置程序失败: {e}")); + log_warn!("启动配置程序失败: {e}"); } } else { - logging::warn("未找到配置程序,无法在启动时拉起"); + log_warn!("未找到配置程序,无法在启动时拉起"); } } @@ -990,10 +1007,7 @@ pub fn run(options: AgentOptions) { let hwnd = match create_agent_window() { Ok(hwnd) => hwnd, Err(e) => { - logging::error(&format!( - "创建代理窗口失败,核心无法启动: {}", - crate::util::win_err(&e) - )); + log_error!("创建代理窗口失败,核心无法启动: {}", crate::util::win_err(&e)); return; } }; @@ -1039,11 +1053,16 @@ pub fn run(options: AgentOptions) { let tray_icons = tray.as_ref().map(|_| TrayIconSet::new()); + // 副作用由专职线程执行,消息循环不被慢操作阻塞。 + let effects_worker = EffectsWorker::spawn(WinEffects::new(exe_dir)); + let mut state = Box::new(AgentState { config, config_path: options.config_path.clone(), recovery_path, - controller: HideController::new(WindowsWindowManager, WinEffects::new(exe_dir)), + controller: HideController::new(WindowsWindowManager, effects_worker.effects()), + effects_worker: Some(effects_worker), + persist_warned: false, tray, tray_icons, ipc_rx, @@ -1056,17 +1075,26 @@ pub fn run(options: AgentOptions) { // 恢复文件存在即上次异常退出仍有窗口被隐藏,先找回。 if let Some(snapshot) = recovery::load(&state.recovery_path) { - logging::warn(&format!( - "检测到上次异常退出,开始找回 {} 个被隐藏的窗口(另需解冻 {} 个进程、取消静音 {} 个进程)", - snapshot.hidden.len(), - snapshot.frozen.len(), - snapshot.muted.len() - )); - state.controller.restore_from(snapshot); + if snapshot.is_restorable(recovery::current_boot_time_ms()) { + logging::warn(&format!( + "检测到上次异常退出,开始找回 {} 个被隐藏的窗口(另需解冻 {} 个、取消静音 {} 个进程)", + snapshot.hidden.len(), + snapshot.frozen.len(), + snapshot.muted.len() + )); + let outcome = state.controller.restore_from(snapshot); + logging::info(&format!( + "崩溃恢复完成:恢复 {} 个窗口,跳过 {} 条失效记录", + outcome.shown, outcome.stale + )); + } else { + // 跨重启(或旧版格式)快照中的句柄与 PID 已失效,只丢弃并留档。 + log_warn!( + "恢复文件来自上一次开机或旧版本,其中的窗口句柄已失效,跳过恢复: {}", + state.recovery_path.display() + ); + } recovery::clear(&state.recovery_path); - // 解冻/取消静音的失败由 effects 各自记录;窗口显示无可靠的失败信号 - // (ShowWindow 的返回值是「先前是否可见」而非成败),故此处只标记流程走完。 - logging::info("崩溃恢复流程已完成"); } unsafe { @@ -1125,6 +1153,11 @@ pub fn run(options: AgentOptions) { let _ = DestroyWindow(hwnd); } + // 非 quit() 路径(如 WM_DESTROY)退出时兜底排干副作用队列。 + if let Some(worker) = state.effects_worker.take() { + worker.shutdown(EFFECTS_SHUTDOWN_TIMEOUT); + } + drop(state); } diff --git a/crates/core/src/effects.rs b/crates/core/src/effects.rs index 94253df..0559b63 100644 --- a/crates/core/src/effects.rs +++ b/crates/core/src/effects.rs @@ -1,16 +1,25 @@ use std::path::PathBuf; +use std::time::Duration; -use crate::{audio, freeze, input, logging}; +use crate::{audio, freeze, input, log_warn, logging}; +/// 隐藏 / 恢复的副作用(静音、冻结、暂停键)。 +/// +/// 实现可以是异步的(如 [`crate::effects_worker::AsyncEffects`]):调用方 +/// 不得依赖「方法返回即动作已生效」,只能依赖调用顺序与执行顺序一致(FIFO)。 pub trait Effects { fn mute(&self, pid: u32, mute: bool); fn suspend(&self, pid: u32, enhanced: bool); fn resume(&self, pid: u32, enhanced: bool); - /// 隐藏前发送媒体「播放/暂停」键。返回是否真的发送了 - /// (仅在检测到有音视频正在播放时才发送)。 - fn send_pause(&self) -> bool; + /// 发送媒体「播放/暂停」键(仅在检测到有音视频正在播放时才发送), + /// 并等待其生效。检测与等待都由实现负责。 + fn send_pause(&self); } +/// 暂停键发出后等待媒体程序响应的时长。冻结须在这之后(FIFO 保证), +/// 否则被冻结的进程收不到按键。 +const SEND_PAUSE_DELAY: Duration = Duration::from_millis(200); + pub struct WinEffects { exe_dir: PathBuf, } @@ -27,19 +36,18 @@ impl Effects for WinEffects { } fn suspend(&self, pid: u32, enhanced: bool) { - // 每一次冻结调用都写日志:成功记 info,失败记 warn,便于事后按 pid 追溯。 if enhanced && freeze::pssuspend_available(&self.exe_dir) { match freeze::suspend_enhanced(&self.exe_dir, pid) { Ok(()) => { - logging::info(&format!("增强冻结成功 (pid={pid})")); + logging::debug(&format!("增强冻结成功 (pid={pid})")); return; } - Err(e) => logging::warn(&format!("增强冻结失败,回退普通冻结 (pid={pid}): {e}")), + Err(e) => log_warn!("增强冻结失败,回退普通冻结 (pid={pid}): {e}"), } } match freeze::suspend_process(pid) { - Ok(()) => logging::info(&format!("普通冻结成功 (pid={pid})")), - Err(e) => logging::warn(&format!("普通冻结失败,该进程未被冻结 (pid={pid}): {e}")), + Ok(()) => logging::debug(&format!("普通冻结成功 (pid={pid})")), + Err(e) => log_warn!("冻结失败,该进程未被冻结 (pid={pid}): {e}"), } } @@ -47,27 +55,24 @@ impl Effects for WinEffects { if enhanced && freeze::pssuspend_available(&self.exe_dir) { match freeze::resume_enhanced(&self.exe_dir, pid) { Ok(()) => { - logging::info(&format!("增强解冻成功 (pid={pid})")); + logging::debug(&format!("增强解冻成功 (pid={pid})")); return; } - Err(e) => logging::warn(&format!("增强解冻失败,回退普通解冻 (pid={pid}): {e}")), + Err(e) => log_warn!("增强解冻失败,回退普通解冻 (pid={pid}): {e}"), } } match freeze::resume_process(pid) { - Ok(()) => logging::info(&format!("普通解冻成功 (pid={pid})")), - // 不升级为 error:进程在隐藏期间正常退出时同样会走到这里(OpenProcess 失败), - // 那是常态而非故障。具体是「已退出」还是「权限不足」由错误码分辨。 - Err(e) => logging::warn(&format!("普通解冻失败 (pid={pid}): {e}")), + Ok(()) => logging::debug(&format!("普通解冻成功 (pid={pid})")), + // 不升级为 error:身份校验后仍可能竞态(解冻前进程恰好退出)。 + Err(e) => log_warn!("解冻失败 (pid={pid}): {e}"), } } - fn send_pause(&self) -> bool { + fn send_pause(&self) { // 没有音视频在播放时不发键,避免把静止的播放器切成播放。 if audio::is_audio_playing() { input::send_media_pause(); - true - } else { - false + std::thread::sleep(SEND_PAUSE_DELAY); } } } diff --git a/crates/core/src/effects_worker.rs b/crates/core/src/effects_worker.rs new file mode 100644 index 0000000..cb37d78 --- /dev/null +++ b/crates/core/src/effects_worker.rs @@ -0,0 +1,159 @@ +//! 副作用专职线程:把静音 / 冻结 / 暂停键等慢操作挪出窗口消息循环。 +//! 任务经 [`AsyncEffects`] 入队,由单线程按 FIFO 顺序执行;通道无界,事件不丢。 + +use std::sync::mpsc::{Sender, channel}; +use std::thread::JoinHandle; +use std::time::{Duration, Instant}; + +use crate::effects::Effects; +use crate::{log_error, log_warn}; + +enum Task { + Mute { pid: u32, mute: bool }, + Suspend { pid: u32, enhanced: bool }, + Resume { pid: u32, enhanced: bool }, + SendPause, + Quit, +} + +/// 副作用线程句柄。`shutdown` 排干队列后退出,保证退出前解冻 / 取消静音已生效。 +pub struct EffectsWorker { + tx: Sender, + handle: Option>, +} + +impl EffectsWorker { + /// 启动专职线程;`inner` 是真正执行副作用的实现(生产为 `WinEffects`)。 + pub fn spawn(inner: E) -> Self { + let (tx, rx) = channel::(); + let handle = std::thread::Builder::new() + .name("bosskey-effects".into()) + .spawn(move || { + while let Ok(task) = rx.recv() { + match task { + Task::Mute { pid, mute } => inner.mute(pid, mute), + Task::Suspend { pid, enhanced } => inner.suspend(pid, enhanced), + Task::Resume { pid, enhanced } => inner.resume(pid, enhanced), + Task::SendPause => inner.send_pause(), + Task::Quit => break, + } + } + }) + .expect("创建副作用线程失败"); + Self { + tx, + handle: Some(handle), + } + } + + /// 供 [`crate::hide::HideController`] 使用的异步 [`Effects`] 句柄。 + pub fn effects(&self) -> AsyncEffects { + AsyncEffects { + tx: self.tx.clone(), + } + } + + /// 排干队列并结束线程;超时放弃等待并 warn。 + pub fn shutdown(mut self, timeout: Duration) { + let _ = self.tx.send(Task::Quit); + let Some(handle) = self.handle.take() else { + return; + }; + let deadline = Instant::now() + timeout; + while !handle.is_finished() { + if Instant::now() >= deadline { + log_warn!("副作用线程未在 {timeout:?} 内排干队列,放弃等待"); + return; + } + std::thread::sleep(Duration::from_millis(10)); + } + let _ = handle.join(); + } +} + +/// 把每个副作用调用转成任务入队的 [`Effects`] 实现。克隆廉价。 +#[derive(Clone)] +pub struct AsyncEffects { + tx: Sender, +} + +impl AsyncEffects { + fn send(&self, task: Task) { + if self.tx.send(task).is_err() { + log_error!("副作用线程已退出,本次副作用未执行"); + } + } +} + +impl Effects for AsyncEffects { + fn mute(&self, pid: u32, mute: bool) { + self.send(Task::Mute { pid, mute }); + } + fn suspend(&self, pid: u32, enhanced: bool) { + self.send(Task::Suspend { pid, enhanced }); + } + fn resume(&self, pid: u32, enhanced: bool) { + self.send(Task::Resume { pid, enhanced }); + } + fn send_pause(&self) { + self.send(Task::SendPause); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::{Arc, Mutex}; + + /// 线程安全的记录用 Effects:按调用顺序记下每个动作。 + #[derive(Clone, Default)] + struct Recorder { + calls: Arc>>, + } + + impl Effects for Recorder { + fn mute(&self, pid: u32, mute: bool) { + self.calls.lock().unwrap().push(format!("mute:{pid}:{mute}")); + } + fn suspend(&self, pid: u32, _enhanced: bool) { + self.calls.lock().unwrap().push(format!("suspend:{pid}")); + } + fn resume(&self, pid: u32, _enhanced: bool) { + self.calls.lock().unwrap().push(format!("resume:{pid}")); + } + fn send_pause(&self) { + self.calls.lock().unwrap().push("pause".into()); + } + } + + #[test] + fn tasks_run_in_fifo_order_and_shutdown_drains_the_queue() { + let recorder = Recorder::default(); + let worker = EffectsWorker::spawn(recorder.clone()); + let effects = worker.effects(); + + // 暂停键必须先于冻结执行(冻结后的进程收不到按键)。 + effects.send_pause(); + effects.mute(100, true); + effects.suspend(100, false); + effects.resume(100, false); + + worker.shutdown(Duration::from_secs(5)); + + assert_eq!( + *recorder.calls.lock().unwrap(), + vec!["pause", "mute:100:true", "suspend:100", "resume:100"], + "任务应按入队顺序全部执行完毕(shutdown 排干队列)" + ); + } + + #[test] + fn send_after_shutdown_does_not_panic() { + let worker = EffectsWorker::spawn(Recorder::default()); + let effects = worker.effects(); + worker.shutdown(Duration::from_secs(5)); + // 线程已退出,入队应静默失败(记日志)而非 panic。 + effects.mute(1, true); + effects.send_pause(); + } +} diff --git a/crates/core/src/hide.rs b/crates/core/src/hide.rs index 5d391a6..de8bd0c 100644 --- a/crates/core/src/hide.rs +++ b/crates/core/src/hide.rs @@ -1,5 +1,4 @@ use std::collections::HashSet; -use std::time::Duration; use bosskey_common::matching::{WindowResolution, match_process_rule, resolve_window_rule}; use bosskey_common::{Config, Setting, WindowInfo}; @@ -7,14 +6,50 @@ use serde::{Deserialize, Serialize}; use crate::effects::Effects; use crate::platform::WindowManager; -use crate::recovery::Snapshot; +use crate::recovery::{ProcRecord, Snapshot}; -const SEND_PAUSE_DELAY: Duration = Duration::from_millis(200); - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +/// 一条隐藏记录。`process_path` / `title` 供日志与排查使用;旧恢复文件缺省为空串。 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Target { pub hwnd: i64, pub pid: u32, + #[serde(default)] + pub process_path: String, + #[serde(default)] + pub title: String, +} + +impl Target { + /// 只有句柄与 PID 的记录(来源没有路径 / 标题信息时使用)。 + pub fn bare(hwnd: i64, pid: u32) -> Self { + Self { + hwnd, + pid, + process_path: String::new(), + title: String::new(), + } + } + + pub fn from_window(w: &WindowInfo) -> Self { + Self { + hwnd: w.hwnd, + pid: w.pid, + process_path: w.path.clone(), + title: w.title.clone(), + } + } + + /// 日志用的一行摘要:`进程名「标题」(hwnd=…, pid=…)`。 + pub fn describe(&self) -> String { + let process = std::path::Path::new(&self.process_path) + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("未知进程"); + format!( + "{process}「{}」(hwnd={}, pid={})", + self.title, self.hwnd, self.pid + ) + } } /// 一条窗口规则的解析结果摘要。 @@ -40,29 +75,20 @@ pub fn resolve_targets( let outcome = match resolve_window_rule(rule, windows) { WindowResolution::Live(w) => { rule.title = w.title.clone(); - result.push(Target { - hwnd: w.hwnd, - pid: w.pid, - }); + result.push(Target::from_window(w)); RuleOutcome::Live } WindowResolution::Reacquired(w) => { rule.hwnd = w.hwnd; rule.pid = w.pid; rule.title = w.title.clone(); - result.push(Target { - hwnd: w.hwnd, - pid: w.pid, - }); + result.push(Target::from_window(w)); RuleOutcome::Reacquired } WindowResolution::Missing => RuleOutcome::Missing, WindowResolution::Regex(hits) => { for w in &hits { - result.push(Target { - hwnd: w.hwnd, - pid: w.pid, - }); + result.push(Target::from_window(w)); } RuleOutcome::Regex(hits.len()) } @@ -72,23 +98,16 @@ pub fn resolve_targets( for rule in &config.process_rules { for w in match_process_rule(rule, windows) { - result.push(Target { - hwnd: w.hwnd, - pid: w.pid, - }); + result.push(Target::from_window(w)); } } if config.setting.hide_current && foreground != 0 { - let pid = windows - .iter() - .find(|w| w.hwnd == foreground) - .map(|w| w.pid) - .unwrap_or(0); - result.push(Target { - hwnd: foreground, - pid, - }); + // 前台窗口可能不在枚举结果里(如工具窗口);缺失的 PID 由 plan_hide 补查。 + match windows.iter().find(|w| w.hwnd == foreground) { + Some(w) => result.push(Target::from_window(w)), + None => result.push(Target::bare(foreground, 0)), + } } let mut seen = HashSet::new(); @@ -107,10 +126,7 @@ pub fn foreground_target(windows: &[WindowInfo], foreground: i64) -> Option Vec { out } +/// 一次隐藏的执行计划:由 [`HideController::plan_hide`] 算出、 +/// [`HideController::commit_hide`] 原样执行,两段之间由调用方落盘意图。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HidePlan { + /// 本次新增的隐藏目标(已剔除死句柄 / 已隐藏项 / 查不到 PID 的项)。 + pub fresh: Vec, + /// 本次新增的静音进程。 + pub mute: Vec, + /// 本次新增的冻结进程。 + pub freeze: Vec, + /// 是否发送媒体暂停键。 + pub send_pause: bool, + /// 本轮冻结方式(首轮跟随设置,之后沿用,保证解冻方式一致)。 + pub enhanced: bool, +} + +/// [`HideController::show`] 的执行结果,供调用方如实记录日志。 +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct ShowOutcome { + /// 实际恢复显示的窗口数。 + pub shown: usize, + /// 因句柄失效或已被其他窗口复用而跳过的记录数。 + pub stale: usize, +} + pub struct HideController { wm: W, effects: E, hidden: Vec, - frozen: Vec, - muted: Vec, + frozen: Vec, + muted: Vec, used_enhanced: bool, } @@ -210,71 +251,165 @@ impl HideController { self.wm.foreground() } - /// 隐藏已解析出的目标:隐藏窗口并按设置应用静音 / 冻结 / 发送暂停键。 - /// `freeze_pids` 为最终要冻结的 PID 集(调用方算好,可能已含递归展开的子进程树, - /// 见 [`freezable_pids`] 与 [`expand_descendants`]);仅在 `freeze_after_hide` 开启时生效。 + /// 计算一次隐藏的执行计划,不做任何窗口 / 副作用动作;顺带完成隐藏集剪枝 + /// 与 PID 补查(仍查不到的目标剔除)。`freeze_pids` 为最终要冻结的 PID 集 + /// (可能已含子进程树),仅在 `freeze_after_hide` 开启时生效。 /// - /// 隐藏是累加的:先前隐藏的窗口留在隐藏集内,`show` 时一并恢复。已在隐藏集 / - /// 静音集 / 冻结集里的目标会被跳过——静音与挂起若重复施加,挂起尤其是计数式的, - /// 需要同样次数的解冻才能恢复。 - pub fn apply_hide(&mut self, setting: &Setting, targets: &[Target], freeze_pids: &[u32]) { + /// 隐藏是累加的,`show` 时一并恢复。已在隐藏 / 静音 / 冻结集内的目标会被 + /// 跳过——挂起是计数式的,重复施加会让解冻次数对不上。 + pub fn plan_hide( + &mut self, + setting: &Setting, + targets: &[Target], + freeze_pids: &[u32], + ) -> HidePlan { + self.prune_stale(); let known: HashSet = self.hidden.iter().map(|t| t.hwnd).collect(); - let fresh: Vec = targets - .iter() - .copied() - .filter(|t| !known.contains(&t.hwnd)) - .collect(); - // 隐藏前先发一次媒体「播放/暂停」键,暂停正在播放的音视频。 - // 只发一次:多目标时重复发送会把「播放/暂停」这个切换键又切回播放。 - // 仅当确有音视频在播放时才真正发键(由 effects 判断),此时才需等待其生效。 - if setting.send_before_hide && !fresh.is_empty() && self.effects.send_pause() { - std::thread::sleep(SEND_PAUSE_DELAY); + let mut fresh: Vec = Vec::new(); + for t in targets { + if known.contains(&t.hwnd) + || fresh.iter().any(|f| f.hwnd == t.hwnd) + || !self.wm.is_window(t.hwnd) + { + continue; + } + let mut t = t.clone(); + if t.pid == 0 { + t.pid = self.wm.window_pid(t.hwnd); + if t.pid == 0 { + continue; + } + } + fresh.push(t); } - for t in &fresh { - self.wm.hide(t.hwnd); - if setting.mute_after_hide && t.pid != 0 && !self.muted.contains(&t.pid) { - self.effects.mute(t.pid, true); - self.muted.push(t.pid); + let mut mute: Vec = Vec::new(); + if setting.mute_after_hide { + for t in &fresh { + if !self.muted.iter().any(|r| r.pid == t.pid) + && !mute.iter().any(|r| r.pid == t.pid) + { + mute.push(self.proc_record(t.pid)); + } } } - // 冻结与目标窗口解耦:freeze_pids 已是最终集合(含子进程树),逐个挂起。 + let mut freeze: Vec = Vec::new(); if setting.freeze_after_hide { - // 冻结方式在解冻时必须与冻结时一致,故只有冻结集为空(本轮是第一次冻结) - // 时才跟随当前设置,之后沿用同一方式。 - if self.frozen.is_empty() { - self.used_enhanced = setting.enhanced_freeze; - } for pid in freeze_pids { - if *pid != 0 && !self.frozen.contains(pid) { - self.effects.suspend(*pid, self.used_enhanced); - self.frozen.push(*pid); + if *pid != 0 + && !self.frozen.iter().any(|r| r.pid == *pid) + && !freeze.iter().any(|r| r.pid == *pid) + { + freeze.push(self.proc_record(*pid)); } } - self.frozen.sort_unstable(); } - self.muted.sort_unstable(); - self.hidden.extend(fresh); + HidePlan { + send_pause: setting.send_before_hide && !fresh.is_empty(), + // 解冻方式必须与冻结时一致:仅首轮冻结跟随设置,之后沿用。 + enhanced: if self.frozen.is_empty() { + setting.enhanced_freeze + } else { + self.used_enhanced + }, + fresh, + mute, + freeze, + } + } + + /// 执行计划:同步隐藏窗口(`SW_HIDE`),静音 / 冻结 / 暂停键经 [`Effects`] 施加 + /// (生产实现为异步队列)。 + pub fn commit_hide(&mut self, plan: HidePlan) { + // 暂停键先入队:冻结后的进程收不到按键。 + if plan.send_pause { + self.effects.send_pause(); + } + + for t in &plan.fresh { + self.wm.hide(t.hwnd); + } + + for r in &plan.mute { + self.effects.mute(r.pid, true); + self.muted.push(*r); + } + self.muted.sort_unstable_by_key(|r| r.pid); + + self.used_enhanced = plan.enhanced; + for r in &plan.freeze { + self.effects.suspend(r.pid, plan.enhanced); + self.frozen.push(*r); + } + self.frozen.sort_unstable_by_key(|r| r.pid); + + self.hidden.extend(plan.fresh); + } + + /// plan + commit 的便捷封装(不需要意图落盘的调用方使用)。 + pub fn apply_hide(&mut self, setting: &Setting, targets: &[Target], freeze_pids: &[u32]) { + let plan = self.plan_hide(setting, targets, freeze_pids); + self.commit_hide(plan); + } + + /// 剔除已不成立的隐藏记录:句柄失效、句柄被别的进程复用(PID 不符)、 + /// 或窗口已重新可见(被外部恢复显示或句柄复用)。 + fn prune_stale(&mut self) { + let wm = &self.wm; + self.hidden.retain(|t| { + wm.is_window(t.hwnd) && wm.window_pid(t.hwnd) == t.pid && !wm.is_visible(t.hwnd) + }); + } + + /// 记录进程身份;创建时刻查不到时记 0,恢复侧对 0 不做校验。 + fn proc_record(&self, pid: u32) -> ProcRecord { + ProcRecord { + pid, + created_at: self.wm.process_start_time(pid), + } + } + + /// 进程记录是否仍指向当初那个进程(PID 会被系统回收复用,须比对创建时刻)。 + fn proc_alive(&self, r: &ProcRecord) -> bool { + let now = self.wm.process_start_time(r.pid); + if now == 0 { + return false; // 进程已退出或查不到。 + } + r.created_at == 0 || now == r.created_at } - pub fn show(&mut self) { - for pid in &self.frozen { - self.effects.resume(*pid, self.used_enhanced); + /// 恢复全部隐藏窗口并撤销副作用;失效记录一律跳过并计入返回值。 + pub fn show(&mut self) -> ShowOutcome { + let mut outcome = ShowOutcome::default(); + + let frozen = std::mem::take(&mut self.frozen); + for r in &frozen { + if self.proc_alive(r) { + self.effects.resume(r.pid, self.used_enhanced); + } } - self.frozen.clear(); - for t in &self.hidden { - self.wm.show(t.hwnd); + let hidden = std::mem::take(&mut self.hidden); + for t in &hidden { + // 句柄仍存活且仍属于当初的进程才恢复,避免弹出复用同一句柄的无关窗口。 + if self.wm.is_window(t.hwnd) && (t.pid == 0 || self.wm.window_pid(t.hwnd) == t.pid) { + self.wm.show(t.hwnd); + outcome.shown += 1; + } else { + outcome.stale += 1; + } } - for pid in &self.muted { - self.effects.mute(*pid, false); + let muted = std::mem::take(&mut self.muted); + for r in &muted { + if self.proc_alive(r) { + self.effects.mute(r.pid, false); + } } - self.muted.clear(); - self.hidden.clear(); + outcome } /// 释放指定进程的隐藏状态:显示窗口、解冻、取消静音,并从隐藏集移除。返回被释放的窗口数。 @@ -284,56 +419,74 @@ impl HideController { } // 先解冻,否则窗口显示出来仍是卡死的。 - let (thaw, keep): (Vec, Vec) = self + let (thaw, keep): (Vec, Vec) = self .frozen .iter() .copied() - .partition(|pid| pids.contains(pid)); - for pid in &thaw { - self.effects.resume(*pid, self.used_enhanced); + .partition(|r| pids.contains(&r.pid)); + for r in &thaw { + if self.proc_alive(r) { + self.effects.resume(r.pid, self.used_enhanced); + } } self.frozen = keep; let (show, keep): (Vec, Vec) = self .hidden .iter() - .copied() + .cloned() .partition(|t| pids.contains(&t.pid)); for t in &show { - self.wm.show(t.hwnd); + if self.wm.is_window(t.hwnd) { + self.wm.show(t.hwnd); + } } self.hidden = keep; - let (unmute, keep): (Vec, Vec) = self + let (unmute, keep): (Vec, Vec) = self .muted .iter() .copied() - .partition(|pid| pids.contains(pid)); - for pid in &unmute { - self.effects.mute(*pid, false); + .partition(|r| pids.contains(&r.pid)); + for r in &unmute { + if self.proc_alive(r) { + self.effects.mute(r.pid, false); + } } self.muted = keep; show.len() } - /// 当前隐藏状态的快照,用于崩溃恢复落盘。 + /// 当前隐藏状态的快照,用于崩溃恢复落盘。版本与开机时刻由 `recovery::save` 盖章。 pub fn snapshot(&self) -> Snapshot { Snapshot { hidden: self.hidden.clone(), frozen: self.frozen.clone(), muted: self.muted.clone(), enhanced: self.used_enhanced, + ..Default::default() } } - /// 从崩溃前的快照恢复:显示窗口、解冻进程、取消静音。 - pub fn restore_from(&mut self, snapshot: Snapshot) { + /// 在当前状态上叠加执行计划后的快照,供意图先行落盘。 + pub fn planned_snapshot(&self, plan: &HidePlan) -> Snapshot { + let mut snapshot = self.snapshot(); + snapshot.hidden.extend(plan.fresh.iter().cloned()); + snapshot.frozen.extend(plan.freeze.iter().copied()); + snapshot.muted.extend(plan.mute.iter().copied()); + snapshot.enhanced = plan.enhanced; + snapshot + } + + /// 从崩溃前的快照恢复。快照整体有效性由调用方先行校验, + /// 逐条身份校验由 [`Self::show`] 完成。 + pub fn restore_from(&mut self, snapshot: Snapshot) -> ShowOutcome { self.hidden = snapshot.hidden; self.frozen = snapshot.frozen; self.muted = snapshot.muted; self.used_enhanced = snapshot.enhanced; - self.show(); + self.show() } } @@ -341,6 +494,7 @@ impl HideController { mod tests { use super::*; use std::cell::RefCell; + use std::collections::HashMap; use bosskey_common::{ProcessRule, WindowRule}; @@ -348,10 +502,25 @@ mod tests { WindowInfo::new(title, hwnd, process, hwnd as u32, path) } + /// 与 `win` 相同,但可指定 PID(用于构造同一进程的多个窗口)。 + fn win_pid(title: &str, hwnd: i64, process: &str, pid: u32, path: &str) -> WindowInfo { + WindowInfo::new(title, hwnd, process, pid, path) + } + fn wrule(title: &str, hwnd: i64, process: &str, path: &str) -> WindowRule { WindowRule::from_window(&win(title, hwnd, process, path)) } + /// 断言用:目标列表压缩为 (hwnd, pid) 序列。 + fn ids(targets: &[Target]) -> Vec<(i64, u32)> { + targets.iter().map(|t| (t.hwnd, t.pid)).collect() + } + + /// Mock 里进程创建时刻的默认值:`1000 + pid`。 + fn start_of(pid: u32) -> i64 { + 1000 + pid as i64 + } + /// 复刻 agent 的隐藏编排:解析目标(含追溯回填)后交给控制器应用副作用。 fn do_hide( controller: &mut HideController, @@ -375,10 +544,23 @@ mod tests { win("记事本", 20, "notepad.exe", "C:\\notepad.exe"), ]; let (targets, outcomes) = resolve_targets(&mut config, &windows, 0); - assert_eq!(targets, vec![Target { hwnd: 10, pid: 10 }]); + assert_eq!(ids(&targets), vec![(10, 10)]); assert_eq!(outcomes, vec![RuleOutcome::Live]); } + #[test] + fn resolved_target_carries_path_and_title() { + let mut config = Config { + window_rules: vec![wrule("微信", 10, "WeChat.exe", "C:\\WeChat.exe")], + ..Default::default() + }; + let windows = vec![win("微信", 10, "WeChat.exe", "C:\\WeChat.exe")]; + let (targets, _) = resolve_targets(&mut config, &windows, 0); + assert_eq!(targets[0].process_path, "C:\\WeChat.exe"); + assert_eq!(targets[0].title, "微信"); + assert_eq!(targets[0].describe(), "WeChat.exe「微信」(hwnd=10, pid=10)"); + } + #[test] fn window_rule_syncs_title_on_hide() { let mut config = Config { @@ -387,7 +569,7 @@ mod tests { }; let windows = vec![win("新标题", 10, "app.exe", "C:\\app.exe")]; let (targets, _) = resolve_targets(&mut config, &windows, 0); - assert_eq!(targets, vec![Target { hwnd: 10, pid: 10 }]); + assert_eq!(ids(&targets), vec![(10, 10)]); assert_eq!( config.window_rules[0].title, "新标题", "隐藏时应同步最新标题" @@ -402,7 +584,7 @@ mod tests { }; let windows = vec![win("微信", 99, "WeChat.exe", "C:\\WeChat.exe")]; let (targets, outcomes) = resolve_targets(&mut config, &windows, 0); - assert_eq!(targets, vec![Target { hwnd: 99, pid: 99 }]); + assert_eq!(ids(&targets), vec![(99, 99)]); assert_eq!(outcomes, vec![RuleOutcome::Reacquired]); assert_eq!(config.window_rules[0].hwnd, 99, "应回填新句柄"); } @@ -436,15 +618,7 @@ mod tests { win("记事本", 20, "notepad.exe", "C:\\notepad.exe"), ]; let (targets, _) = resolve_targets(&mut config, &windows, 0); - assert_eq!( - targets, - vec![Target { hwnd: 10, pid: 10 }, Target { hwnd: 11, pid: 11 }] - ); - } - - /// 与 `win` 相同,但可指定 PID(用于构造同一进程的多个窗口)。 - fn win_pid(title: &str, hwnd: i64, process: &str, pid: u32, path: &str) -> WindowInfo { - WindowInfo::new(title, hwnd, process, pid, path) + assert_eq!(ids(&targets), vec![(10, 10), (11, 11)]); } #[test] @@ -453,7 +627,7 @@ mod tests { win_pid("微信", 10, "WeChat.exe", 500, "C:\\WeChat.exe"), win_pid("文件传输助手", 11, "WeChat.exe", 500, "C:\\WeChat.exe"), ]; - let targets = vec![Target { hwnd: 10, pid: 500 }]; + let targets = vec![Target::bare(10, 500)]; assert!( freezable_pids(&targets, &windows).is_empty(), "还有窗口开着时不应冻结" @@ -466,7 +640,7 @@ mod tests { win_pid("微信", 10, "WeChat.exe", 500, "C:\\WeChat.exe"), win_pid("文件传输助手", 11, "WeChat.exe", 500, "C:\\WeChat.exe"), ]; - let targets = vec![Target { hwnd: 10, pid: 500 }, Target { hwnd: 11, pid: 500 }]; + let targets = vec![Target::bare(10, 500), Target::bare(11, 500)]; assert_eq!(freezable_pids(&targets, &windows), vec![500]); } @@ -476,7 +650,7 @@ mod tests { win_pid("微信", 10, "WeChat.exe", 500, "C:\\WeChat.exe"), win_pid("后台窗口", 11, "WeChat.exe", 500, "C:\\WeChat.exe").with_visibility(false), ]; - let targets = vec![Target { hwnd: 10, pid: 500 }]; + let targets = vec![Target::bare(10, 500)]; assert_eq!(freezable_pids(&targets, &windows), vec![500]); } @@ -556,34 +730,48 @@ mod tests { win("记事本", 20, "notepad.exe", "C:\\notepad.exe"), ]; let (targets, _) = resolve_targets(&mut config.clone(), &windows, 20); - assert_eq!( - targets, - vec![Target { hwnd: 10, pid: 10 }, Target { hwnd: 20, pid: 20 }] - ); + assert_eq!(ids(&targets), vec![(10, 10), (20, 20)]); let (same, _) = resolve_targets(&mut config, &windows, 10); - assert_eq!( - same, - vec![Target { hwnd: 10, pid: 10 }], - "前台与已命中窗口相同应去重" - ); + assert_eq!(ids(&same), vec![(10, 10)], "前台与已命中窗口相同应去重"); } struct MockWm { windows: Vec, foreground: i64, visible: RefCell>, + /// 仍然存在的句柄集合(窗口销毁 = 从中移除;与 visible 是两回事)。 + exists: RefCell>, + /// 覆写某句柄当前所属的 PID(模拟句柄被别的窗口复用)。 + pid_overrides: RefCell>, + /// 覆写某进程的创建时刻(模拟 PID 被回收复用;0 = 进程已退出)。 + start_overrides: RefCell>, } impl MockWm { fn new(windows: Vec, foreground: i64) -> Self { - let visible = windows.iter().map(|w| w.hwnd).collect(); + let handles: HashSet = windows.iter().map(|w| w.hwnd).collect(); Self { windows, foreground, - visible: RefCell::new(visible), + visible: RefCell::new(handles.clone()), + exists: RefCell::new(handles), + pid_overrides: RefCell::new(HashMap::new()), + start_overrides: RefCell::new(HashMap::new()), } } + + /// 模拟窗口被销毁:句柄失效且不可见。 + fn destroy(&self, hwnd: i64) { + self.visible.borrow_mut().remove(&hwnd); + self.exists.borrow_mut().remove(&hwnd); + } + + /// 模拟句柄被系统回收后分配给了新窗口。 + fn revive(&self, hwnd: i64) { + self.exists.borrow_mut().insert(hwnd); + self.visible.borrow_mut().insert(hwnd); + } } impl WindowManager for MockWm { @@ -607,6 +795,32 @@ mod tests { fn foreground(&self) -> i64 { self.foreground } + fn is_window(&self, hwnd: i64) -> bool { + self.exists.borrow().contains(&hwnd) + } + fn window_pid(&self, hwnd: i64) -> u32 { + if !self.is_window(hwnd) { + return 0; + } + if let Some(pid) = self.pid_overrides.borrow().get(&hwnd) { + return *pid; + } + self.windows + .iter() + .find(|w| w.hwnd == hwnd) + .map(|w| w.pid) + .unwrap_or(0) + } + fn process_start_time(&self, pid: u32) -> i64 { + if pid == 0 { + return 0; + } + self.start_overrides + .borrow() + .get(&pid) + .copied() + .unwrap_or(start_of(pid)) + } } #[derive(Default)] @@ -627,9 +841,8 @@ mod tests { fn resume(&self, pid: u32, _enhanced: bool) { self.resumes.borrow_mut().push(pid); } - fn send_pause(&self) -> bool { + fn send_pause(&self) { *self.pauses.borrow_mut() += 1; - true } } @@ -659,7 +872,8 @@ mod tests { assert_eq!(*controller.effects.suspends.borrow(), vec![10]); assert_eq!(*controller.effects.pauses.borrow(), 1, "应发送一次暂停键"); - controller.show(); + let outcome = controller.show(); + assert_eq!(outcome, ShowOutcome { shown: 1, stale: 0 }); assert!(!controller.is_hidden()); assert!(controller.wm.is_visible(10), "恢复后微信应可见"); assert_eq!(*controller.effects.resumes.borrow(), vec![10], "应解冻"); @@ -688,8 +902,8 @@ mod tests { ); let mut controller = HideController::new(wm, MockEffects::default()); - controller.apply_hide(&setting, &[Target { hwnd: 10, pid: 10 }], &[]); - controller.apply_hide(&setting, &[Target { hwnd: 20, pid: 20 }], &[]); + controller.apply_hide(&setting, &[Target::bare(10, 10)], &[]); + controller.apply_hide(&setting, &[Target::bare(20, 20)], &[]); assert_eq!( controller.hidden_count(), @@ -722,7 +936,7 @@ mod tests { let wm = MockWm::new(vec![win("微信", 10, "WeChat.exe", "C:\\WeChat.exe")], 10); let mut controller = HideController::new(wm, MockEffects::default()); - let targets = [Target { hwnd: 10, pid: 10 }]; + let targets = [Target::bare(10, 10)]; controller.apply_hide(&setting, &targets, &[10]); controller.apply_hide(&setting, &targets, &[10]); @@ -750,8 +964,8 @@ mod tests { win_pid("已隐藏", 11, "app.exe", 600, "C:\\app.exe").with_visibility(false), ]; assert_eq!( - foreground_target(&windows, 10), - Some(Target { hwnd: 10, pid: 500 }) + foreground_target(&windows, 10).map(|t| (t.hwnd, t.pid)), + Some((10, 500)) ); assert_eq!(foreground_target(&windows, 0), None, "无前台窗口"); assert_eq!( @@ -811,27 +1025,75 @@ mod tests { do_hide(&mut controller, &mut config); let snapshot = controller.snapshot(); - assert_eq!(snapshot.hidden, vec![Target { hwnd: 10, pid: 10 }]); - assert_eq!(snapshot.frozen, vec![10]); - assert_eq!(snapshot.muted, vec![10]); + assert_eq!(ids(&snapshot.hidden), vec![(10, 10)]); + assert_eq!( + snapshot.frozen, + vec![ProcRecord { + pid: 10, + created_at: start_of(10) + }], + "冻结记录应带进程创建时刻" + ); + assert_eq!( + snapshot.muted, + vec![ProcRecord { + pid: 10, + created_at: start_of(10) + }] + ); controller.show(); assert!(controller.snapshot().is_empty(), "显示后快照应清空"); } + #[test] + fn planned_snapshot_matches_state_after_commit() { + let mut config = Config::default(); + config.setting.hide_current = false; + config.setting.mute_after_hide = true; + config.setting.freeze_after_hide = true; + config.window_rules = vec![wrule("微信", 10, "WeChat.exe", "C:\\WeChat.exe")]; + + let wm = MockWm::new(vec![win("微信", 10, "WeChat.exe", "C:\\WeChat.exe")], 10); + let mut controller = HideController::new(wm, MockEffects::default()); + + let windows = controller.enumerate(); + let (targets, _) = resolve_targets(&mut config, &windows, 0); + let freezable = freezable_pids(&targets, &windows); + + let plan = controller.plan_hide(&config.setting, &targets, &freezable); + let planned = controller.planned_snapshot(&plan); + controller.commit_hide(plan); + let actual = controller.snapshot(); + + // 意图快照与提交后的实际状态一致——落盘发生在动作前,两者必须等价。 + assert_eq!(planned.hidden, actual.hidden); + assert_eq!(planned.frozen, actual.frozen); + assert_eq!(planned.muted, actual.muted); + assert_eq!(planned.enhanced, actual.enhanced); + } + #[test] fn restore_from_snapshot_shows_windows_and_reverts_effects() { let wm = MockWm::new(vec![win("微信", 10, "WeChat.exe", "C:\\WeChat.exe")], 10); wm.hide(10); let mut controller = HideController::new(wm, MockEffects::default()); - controller.restore_from(Snapshot { - hidden: vec![Target { hwnd: 10, pid: 10 }], - frozen: vec![10], - muted: vec![10], + let outcome = controller.restore_from(Snapshot { + hidden: vec![Target::bare(10, 10)], + frozen: vec![ProcRecord { + pid: 10, + created_at: start_of(10), + }], + muted: vec![ProcRecord { + pid: 10, + created_at: start_of(10), + }], enhanced: false, + ..Default::default() }); + assert_eq!(outcome, ShowOutcome { shown: 1, stale: 0 }); assert!(!controller.is_hidden(), "恢复完成后应回到未隐藏状态"); assert!(controller.wm.is_visible(10), "崩溃前隐藏的窗口应被找回"); assert_eq!(*controller.effects.resumes.borrow(), vec![10], "应解冻进程"); @@ -843,6 +1105,128 @@ mod tests { assert!(controller.snapshot().is_empty()); } + #[test] + fn dead_handle_is_pruned_and_reused_handle_can_hide_again() { + let setting = Setting { + hide_current: false, + ..Setting::default() + }; + let wm = MockWm::new(vec![win("微信", 10, "WeChat.exe", "C:\\WeChat.exe")], 10); + let mut controller = HideController::new(wm, MockEffects::default()); + + controller.apply_hide(&setting, &[Target::bare(10, 10)], &[]); + assert_eq!(controller.hidden_count(), 1); + + // 旧窗口销毁,句柄随后被新窗口复用。 + controller.wm.destroy(10); + controller.wm.revive(10); + + controller.apply_hide(&setting, &[Target::bare(10, 10)], &[]); + assert_eq!( + controller.hidden_count(), + 1, + "死记录应被剪掉,复用句柄的新窗口不应被误判为已隐藏" + ); + assert!(!controller.wm.is_visible(10), "新窗口应真的被隐藏"); + } + + #[test] + fn show_skips_destroyed_and_reused_handles() { + let setting = Setting { + hide_current: false, + ..Setting::default() + }; + let wm = MockWm::new( + vec![ + win("微信", 10, "WeChat.exe", "C:\\WeChat.exe"), + win("记事本", 20, "notepad.exe", "C:\\notepad.exe"), + win("画图", 30, "mspaint.exe", "C:\\mspaint.exe"), + ], + 10, + ); + let mut controller = HideController::new(wm, MockEffects::default()); + controller.apply_hide( + &setting, + &[ + Target::bare(10, 10), + Target::bare(20, 20), + Target::bare(30, 30), + ], + &[], + ); + + controller.wm.destroy(10); // 窗口已退出。 + controller.wm.pid_overrides.borrow_mut().insert(20, 999); // 句柄被别的进程复用。 + + let outcome = controller.show(); + assert_eq!( + outcome, + ShowOutcome { shown: 1, stale: 2 }, + "死句柄与被复用句柄都应跳过" + ); + assert!(!controller.wm.is_visible(20), "被复用的句柄不得被弹出来"); + assert!(controller.wm.is_visible(30), "正常窗口应恢复"); + } + + #[test] + fn resume_is_skipped_when_pid_was_recycled_or_process_exited() { + let setting = Setting { + hide_current: false, + mute_after_hide: false, + freeze_after_hide: true, + ..Setting::default() + }; + let wm = MockWm::new( + vec![ + win_pid("A", 10, "a.exe", 100, "C:\\a.exe"), + win_pid("B", 20, "b.exe", 200, "C:\\b.exe"), + ], + 0, + ); + let mut controller = HideController::new(wm, MockEffects::default()); + controller.apply_hide( + &setting, + &[Target::bare(10, 100), Target::bare(20, 200)], + &[100, 200], + ); + + // PID 100 被回收给了新进程(创建时刻变了);PID 200 的进程已退出。 + controller + .wm + .start_overrides + .borrow_mut() + .insert(100, 9_999); + controller.wm.start_overrides.borrow_mut().insert(200, 0); + + controller.show(); + assert!( + controller.effects.resumes.borrow().is_empty(), + "身份不符或已退出的进程不得解冻,避免干扰无关进程" + ); + } + + #[test] + fn plan_fills_missing_pid_and_drops_unresolvable_targets() { + let setting = Setting { + hide_current: false, + ..Setting::default() + }; + let wm = MockWm::new( + vec![win_pid("画图", 30, "mspaint.exe", 33, "C:\\mspaint.exe")], + 0, + ); + let mut controller = HideController::new(wm, MockEffects::default()); + + // hwnd 40 不存在(is_window=false),hwnd 30 的 pid 现场补查为 33。 + controller.apply_hide(&setting, &[Target::bare(30, 0), Target::bare(40, 0)], &[]); + assert_eq!(controller.hidden_count(), 1, "查无此窗的目标应被剔除"); + assert_eq!( + controller.release_pids(&[33]), + 1, + "补齐 PID 后 release_pids 应能按进程释放该窗口" + ); + } + #[test] fn expand_descendants_collects_multi_level_tree() { // 1 → 2 → 4, 1 → 3;另有无关的 9 → 10。 diff --git a/crates/core/src/i18n.rs b/crates/core/src/i18n.rs index db163b2..fd03cbf 100644 --- a/crates/core/src/i18n.rs +++ b/crates/core/src/i18n.rs @@ -21,6 +21,7 @@ pub enum Msg { HiddenBody, ShownBody, ConfigExeMissing, + RecoveryPersistFailedBody, AutostartOffTitle, AutostartOffBody, AutostartOnTitle, @@ -67,6 +68,7 @@ impl Msg { Msg::HiddenBody => "已隐藏窗口", Msg::ShownBody => "已恢复显示窗口", Msg::ConfigExeMissing => "未找到配置程序", + Msg::RecoveryPersistFailedBody => "无法写入崩溃恢复文件,异常退出后将无法自动找回窗口", Msg::AutostartOffTitle => "开机自启已关闭", Msg::AutostartOffBody => "Boss Key 将不再随系统启动", Msg::AutostartOnTitle => "开机自启已开启", @@ -104,6 +106,7 @@ impl Msg { Msg::HiddenBody => "Windows hidden", Msg::ShownBody => "Windows restored", Msg::ConfigExeMissing => "Settings app not found", + Msg::RecoveryPersistFailedBody => "Cannot write the crash-recovery file; windows cannot be restored automatically after an abnormal exit", Msg::AutostartOffTitle => "Startup disabled", Msg::AutostartOffBody => "Boss Key will no longer start with Windows", Msg::AutostartOnTitle => "Startup enabled", @@ -141,6 +144,7 @@ impl Msg { Msg::HiddenBody => "已隱藏視窗", Msg::ShownBody => "已復原顯示視窗", Msg::ConfigExeMissing => "找不到設定程式", + Msg::RecoveryPersistFailedBody => "無法寫入當機復原檔案,異常結束後將無法自動找回視窗", Msg::AutostartOffTitle => "已關閉開機自動啟動", Msg::AutostartOffBody => "Boss Key 將不再隨系統啟動", Msg::AutostartOnTitle => "已開啟開機自動啟動", @@ -214,7 +218,7 @@ mod tests { use super::*; /// 全部文案键;新增 Msg 变体后必须同步登记,否则跨语言校验会漏掉它。 - const ALL_MSGS: [Msg; 32] = [ + const ALL_MSGS: [Msg; 33] = [ Msg::MenuSettings, Msg::MenuShowWindows, Msg::MenuHideWindows, @@ -226,6 +230,7 @@ mod tests { Msg::HiddenBody, Msg::ShownBody, Msg::ConfigExeMissing, + Msg::RecoveryPersistFailedBody, Msg::AutostartOffTitle, Msg::AutostartOffBody, Msg::AutostartOnTitle, diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 1e10722..7570b5e 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -2,6 +2,7 @@ pub mod agent; pub mod audio; pub mod autostart; pub mod effects; +pub mod effects_worker; pub mod elevation; pub mod float_window; pub mod freeze; diff --git a/crates/core/src/logging.rs b/crates/core/src/logging.rs index 6345595..bbcc9d9 100644 --- a/crates/core/src/logging.rs +++ b/crates/core/src/logging.rs @@ -180,6 +180,16 @@ pub fn error(message: &str) { log(Level::Error, message); } +/// 供 [`crate::log_warn!`] 使用:warn 并附上报错位置。 +pub fn warn_at(file: &str, line: u32, message: &str) { + log(Level::Warn, &format!("{message} ({file}:{line})")); +} + +/// 供 [`crate::log_error!`] 使用:error 并附上报错位置。 +pub fn error_at(file: &str, line: u32, message: &str) { + log(Level::Error, &format!("{message} ({file}:{line})")); +} + fn log(level: Level, message: &str) { if !should_emit(level) { return; @@ -192,6 +202,22 @@ fn log(level: Level, message: &str) { } } +/// warn 级日志并自动附上调用处的 `文件:行号`。 +#[macro_export] +macro_rules! log_warn { + ($($arg:tt)*) => { + $crate::logging::warn_at(file!(), line!(), &format!($($arg)*)) + }; +} + +/// error 级日志并自动附上调用处的 `文件:行号`。 +#[macro_export] +macro_rules! log_error { + ($($arg:tt)*) => { + $crate::logging::error_at(file!(), line!(), &format!($($arg)*)) + }; +} + /// 格式化 panic 信息为单条日志(便于单元测试)。 fn format_panic(message: &str, location: Option<&str>) -> String { match location { @@ -336,6 +362,17 @@ mod tests { assert!(!dir.join("logs").exists(), "关闭日志时不应创建目录"); } + #[test] + fn warn_at_appends_source_location() { + let dir = temp_dir(); + let logger = Logger::new(dir.clone(), 7); + // 直接验证格式化结果(warn_at 走全局 logger,测试里用本地实例复刻其格式)。 + logger.log(Level::Warn, &format!("{} ({}:{})", "落盘失败", "agent.rs", 42)); + let logs: Vec<_> = fs::read_dir(&dir).unwrap().flatten().collect(); + let content = fs::read_to_string(logs[0].path()).unwrap(); + assert!(content.contains("[WARN] 落盘失败 (agent.rs:42)")); + } + #[test] fn panic_message_includes_location_when_available() { assert_eq!( diff --git a/crates/core/src/platform/mod.rs b/crates/core/src/platform/mod.rs index 5f0d8f7..42b6a9f 100644 --- a/crates/core/src/platform/mod.rs +++ b/crates/core/src/platform/mod.rs @@ -8,6 +8,12 @@ pub trait WindowManager { fn show(&self, hwnd: i64); fn is_visible(&self, hwnd: i64) -> bool; fn foreground(&self) -> i64; + /// 句柄当前是否仍指向一个存在的窗口(句柄值会被系统回收复用)。 + fn is_window(&self, hwnd: i64) -> bool; + /// 句柄当前所属进程的 PID;查不到返回 0。 + fn window_pid(&self, hwnd: i64) -> u32; + /// 进程创建时刻(Unix 毫秒),用于识别 PID 复用;查不到返回 0。 + fn process_start_time(&self, pid: u32) -> i64; } pub fn manager() -> impl WindowManager { diff --git a/crates/core/src/platform/win32.rs b/crates/core/src/platform/win32.rs index b86802d..86e2e86 100644 --- a/crates/core/src/platform/win32.rs +++ b/crates/core/src/platform/win32.rs @@ -1,14 +1,15 @@ use std::ffi::c_void; use bosskey_common::{NO_TITLE, WindowInfo}; -use windows::Win32::Foundation::{CloseHandle, HWND, LPARAM}; +use windows::Win32::Foundation::{CloseHandle, FILETIME, HWND, LPARAM}; use windows::Win32::System::Threading::{ - OpenProcess, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION, QueryFullProcessImageNameW, + GetProcessTimes, OpenProcess, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION, + QueryFullProcessImageNameW, }; use windows::Win32::UI::WindowsAndMessaging::{ EnumWindows, GW_OWNER, GWL_EXSTYLE, GetForegroundWindow, GetWindow, GetWindowLongPtrW, - GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId, IsWindowVisible, SW_HIDE, - SW_SHOW, ShowWindow, WS_EX_APPWINDOW, WS_EX_TOOLWINDOW, + GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId, IsWindow, IsWindowVisible, + SW_HIDE, SW_SHOW, ShowWindow, WS_EX_APPWINDOW, WS_EX_TOOLWINDOW, }; use windows::core::{BOOL, PWSTR}; @@ -77,6 +78,35 @@ pub(crate) fn process_path(pid: u32) -> String { } } +/// FILETIME(1601 起 100ns)→ Unix 毫秒。 +fn filetime_to_unix_ms(ft: FILETIME) -> i64 { + const EPOCH_DIFF_100NS: i64 = 116_444_736_000_000_000; + let raw = ((ft.dwHighDateTime as i64) << 32) | ft.dwLowDateTime as i64; + (raw - EPOCH_DIFF_100NS) / 10_000 +} + +/// 进程创建时刻(Unix 毫秒);打不开进程时返回 0。 +pub(crate) fn process_start_time(pid: u32) -> i64 { + if pid == 0 { + return 0; + } + unsafe { + let Ok(handle) = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) else { + return 0; + }; + let mut created = FILETIME::default(); + let mut exited = FILETIME::default(); + let mut kernel = FILETIME::default(); + let mut user = FILETIME::default(); + let result = GetProcessTimes(handle, &mut created, &mut exited, &mut kernel, &mut user); + let _ = CloseHandle(handle); + match result { + Ok(()) => filetime_to_unix_ms(created), + Err(_) => 0, + } + } +} + fn process_name_from_path(path: &str) -> String { std::path::Path::new(path) .file_name() @@ -151,6 +181,18 @@ impl WindowManager for WindowsWindowManager { fn foreground(&self) -> i64 { unsafe { hwnd_to_i64(GetForegroundWindow()) } } + + fn is_window(&self, hwnd: i64) -> bool { + unsafe { IsWindow(Some(hwnd_from(hwnd))).as_bool() } + } + + fn window_pid(&self, hwnd: i64) -> u32 { + window_pid(hwnd_from(hwnd)) + } + + fn process_start_time(&self, pid: u32) -> i64 { + process_start_time(pid) + } } #[cfg(test)] @@ -235,4 +277,59 @@ mod tests { let _ = DestroyWindow(hwnd); } } + + #[test] + fn filetime_epoch_maps_to_zero() { + // 1970-01-01 对应的 FILETIME 值应换算为 0 毫秒。 + let ft = FILETIME { + dwLowDateTime: (116_444_736_000_000_000u64 & 0xFFFF_FFFF) as u32, + dwHighDateTime: (116_444_736_000_000_000u64 >> 32) as u32, + }; + assert_eq!(filetime_to_unix_ms(ft), 0); + } + + #[test] + fn dead_handle_and_pid_are_detected() { + unsafe { + let hwnd = CreateWindowExW( + WINDOW_EX_STYLE(0), + w!("Static"), + w!("BossKeyIdentityTestWindow"), + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + 100, + 80, + None, + None, + None, + None, + ) + .expect("创建测试窗口失败"); + let wm = WindowsWindowManager; + let id = hwnd_to_i64(hwnd); + + assert!(wm.is_window(id), "存活窗口应通过 IsWindow"); + assert_eq!( + wm.window_pid(id), + std::process::id(), + "自建窗口应属于本进程" + ); + + let _ = DestroyWindow(hwnd); + assert!(!wm.is_window(id), "销毁后句柄应判定失效"); + assert_eq!(wm.window_pid(id), 0, "失效句柄查不到 PID"); + } + } + + #[test] + fn own_process_start_time_is_recent_and_stable() { + let wm = WindowsWindowManager; + let t1 = wm.process_start_time(std::process::id()); + let t2 = wm.process_start_time(std::process::id()); + assert!(t1 > 0, "本进程创建时刻应可查询"); + assert_eq!(t1, t2, "同一进程两次查询应一致"); + assert_eq!(wm.process_start_time(0), 0, "PID 0 应返回 0"); + assert_eq!(wm.process_start_time(0xFFFF_FFF0), 0, "无效 PID 应返回 0"); + } } diff --git a/crates/core/src/recovery.rs b/crates/core/src/recovery.rs index 151b459..7bf19a3 100644 --- a/crates/core/src/recovery.rs +++ b/crates/core/src/recovery.rs @@ -1,19 +1,50 @@ //! 崩溃恢复:隐藏状态落盘,核心异常退出后下次启动自动找回窗口。 +//! +//! 写入在隐藏动作执行前发生(意图先行),走 tmp + rename 原子替换; +//! 快照带开机时刻与进程创建时刻,跨重启后失效的快照在加载侧被丢弃。 -use std::path::Path; +use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use crate::hide::Target; +use crate::log_warn; /// 恢复文件名(与 config.json 同目录)。 pub const RECOVERY_FILE_NAME: &str = "recovery.json"; +/// 当前快照格式版本。旧版(无该字段,缺省 0)不含身份信息,加载时按不可信丢弃。 +pub const SCHEMA_CURRENT: u32 = 1; + +/// 判定「同一次开机」的容差(GetTickCount64 精度约 10–16ms,另留时钟漂移余量)。 +const BOOT_TOLERANCE_MS: i64 = 5_000; + +/// 带创建时刻的进程记录(PID 会被系统回收复用,须配合创建时刻标识进程)。 +/// `created_at` 为 0 表示记录时查不到,恢复时不做身份校验。 +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProcRecord { + pub pid: u32, + #[serde(default)] + pub created_at: i64, +} + +impl ProcRecord { + pub fn bare(pid: u32) -> Self { + Self { pid, created_at: 0 } + } +} + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct Snapshot { + /// 快照格式版本;由 [`save`] 盖章,加载侧与 [`SCHEMA_CURRENT`] 不符即丢弃。 + #[serde(default)] + pub schema: u32, + /// 写入时刻推算出的本次开机时刻(Unix 毫秒);由 [`save`] 盖章。 + #[serde(default)] + pub boot_time_ms: i64, pub hidden: Vec, - pub frozen: Vec, - pub muted: Vec, + pub frozen: Vec, + pub muted: Vec, pub enhanced: bool, } @@ -22,23 +53,67 @@ impl Snapshot { pub fn is_empty(&self) -> bool { self.hidden.is_empty() && self.frozen.is_empty() && self.muted.is_empty() } + + /// 快照是否可用于恢复:格式为当前版本,且写入时与现在处于同一次开机。 + pub fn is_restorable(&self, now_boot_time_ms: i64) -> bool { + self.schema == SCHEMA_CURRENT && same_boot(self.boot_time_ms, now_boot_time_ms) + } +} + +/// 由「当前时间 − 开机以来的毫秒数」推算本次开机时刻(Unix 毫秒)。 +pub fn current_boot_time_ms() -> i64 { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as i64) + .unwrap_or(0); + let uptime = unsafe { windows::Win32::System::SystemInformation::GetTickCount64() } as i64; + now - uptime } -/// 保存快照。快照为空时等价于清除(避免留下无意义的文件)。 +/// 两个推算出的开机时刻是否指同一次开机(容差内)。 +pub fn same_boot(a: i64, b: i64) -> bool { + (a - b).abs() <= BOOT_TOLERANCE_MS +} + +/// 保存快照:盖上版本与开机时刻,写入 tmp 后 rename 原子替换。 +/// 快照为空时等价于清除。 pub fn save(path: &Path, snapshot: &Snapshot) -> std::io::Result<()> { if snapshot.is_empty() { clear(path); return Ok(()); } - let json = serde_json::to_string_pretty(snapshot) + let mut stamped = snapshot.clone(); + stamped.schema = SCHEMA_CURRENT; + stamped.boot_time_ms = current_boot_time_ms(); + let json = serde_json::to_string_pretty(&stamped) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; - std::fs::write(path, json) + let tmp = tmp_path(path); + std::fs::write(&tmp, json)?; + // Windows 下 rename 走 MOVEFILE_REPLACE_EXISTING,同目录替换是原子的。 + std::fs::rename(&tmp, path) +} + +fn tmp_path(path: &Path) -> PathBuf { + let mut name = path.file_name().unwrap_or_default().to_os_string(); + name.push(".tmp"); + path.with_file_name(name) } -/// 读取快照。文件不存在、损坏或内容为空时返回 None。 +/// 读取快照。文件不存在或内容为空时返回 None;损坏时改名保留现场并 warn。 pub fn load(path: &Path) -> Option { let content = std::fs::read_to_string(path).ok()?; - let snapshot: Snapshot = serde_json::from_str(&content).ok()?; + let snapshot: Snapshot = match serde_json::from_str(&content) { + Ok(s) => s, + Err(e) => { + let corrupt = corrupt_path(path); + let _ = std::fs::rename(path, &corrupt); + log_warn!( + "恢复文件解析失败,本次不恢复;原文件已改名为 {}: {e}", + corrupt.display() + ); + return None; + } + }; if snapshot.is_empty() { None } else { @@ -46,6 +121,12 @@ pub fn load(path: &Path) -> Option { } } +fn corrupt_path(path: &Path) -> PathBuf { + let mut name = path.file_name().unwrap_or_default().to_os_string(); + name.push(".corrupt"); + path.with_file_name(name) +} + /// 删除恢复文件(不存在时静默成功)。 pub fn clear(path: &Path) { let _ = std::fs::remove_file(path); @@ -65,19 +146,32 @@ mod tests { fn sample() -> Snapshot { Snapshot { - hidden: vec![Target { hwnd: 10, pid: 100 }, Target { hwnd: 20, pid: 200 }], - frozen: vec![100], - muted: vec![200], + hidden: vec![Target::bare(10, 100), Target::bare(20, 200)], + frozen: vec![ProcRecord { + pid: 100, + created_at: 111, + }], + muted: vec![ProcRecord::bare(200)], enhanced: true, + ..Default::default() } } #[test] - fn save_load_round_trip() { + fn save_load_round_trip_and_stamps_identity() { let path = temp_file(); - let snapshot = sample(); - save(&path, &snapshot).expect("保存快照应成功"); - assert_eq!(load(&path), Some(snapshot)); + save(&path, &sample()).expect("保存快照应成功"); + let loaded = load(&path).expect("应能读回"); + assert_eq!(loaded.hidden, sample().hidden); + assert_eq!(loaded.frozen, sample().frozen); + assert_eq!(loaded.muted, sample().muted); + assert_eq!(loaded.schema, SCHEMA_CURRENT, "保存时应盖上版本"); + assert!( + same_boot(loaded.boot_time_ms, current_boot_time_ms()), + "保存时应盖上本次开机时刻" + ); + assert!(loaded.is_restorable(current_boot_time_ms())); + assert!(!path.with_file_name("recovery.json.tmp").exists()); } #[test] @@ -89,10 +183,35 @@ mod tests { } #[test] - fn load_corrupt_file_returns_none() { + fn load_corrupt_file_renames_it_for_inspection() { let path = temp_file(); std::fs::write(&path, "{ not valid json !!").unwrap(); assert_eq!(load(&path), None, "损坏文件应按无快照处理而非 panic"); + assert!(!path.exists(), "损坏文件不应留在原名"); + assert!( + corrupt_path(&path).exists(), + "损坏文件应改名保留现场供排查" + ); + } + + #[test] + fn snapshot_from_previous_boot_is_not_restorable() { + let now = current_boot_time_ms(); + let mut snapshot = sample(); + snapshot.schema = SCHEMA_CURRENT; + snapshot.boot_time_ms = now - 3_600_000; // 一小时前的「开机」 + assert!(!snapshot.is_restorable(now), "跨重启的快照不可恢复"); + snapshot.boot_time_ms = now - 1_000; // 容差内 + assert!(snapshot.is_restorable(now)); + } + + #[test] + fn legacy_snapshot_without_schema_is_not_restorable() { + // 旧版快照(frozen/muted 是裸 PID 数组)应能解析但不可恢复。 + let json = r#"{"hidden":[{"hwnd":10,"pid":100}],"frozen":[],"muted":[],"enhanced":false}"#; + let snapshot: Snapshot = serde_json::from_str(json).unwrap(); + assert_eq!(snapshot.schema, 0); + assert!(!snapshot.is_restorable(current_boot_time_ms())); } #[test] @@ -112,4 +231,12 @@ mod tests { assert!(!path.exists()); clear(&path); } + + #[test] + fn same_boot_respects_tolerance() { + assert!(same_boot(1_000_000, 1_000_000)); + assert!(same_boot(1_000_000, 1_000_000 + BOOT_TOLERANCE_MS)); + assert!(!same_boot(1_000_000, 1_000_000 + BOOT_TOLERANCE_MS + 1)); + assert!(!same_boot(1_000_000, 995_000 - 1)); + } } diff --git a/crates/core/tests/agent_recovery.rs b/crates/core/tests/agent_recovery.rs index ef58598..1b5200d 100644 --- a/crates/core/tests/agent_recovery.rs +++ b/crates/core/tests/agent_recovery.rs @@ -66,13 +66,16 @@ fn agent_restores_hidden_windows_left_by_a_crash() { .unwrap(); let recovery_path = dir.path().join(recovery::RECOVERY_FILE_NAME); + // save 会盖上版本与本次开机时刻,等价于核心崩溃前留下的真实快照。 + // 测试窗口属于本进程,pid 须如实填写,否则恢复侧的身份校验会拦下它。 recovery::save( &recovery_path, &Snapshot { - hidden: vec![Target { hwnd, pid: 0 }], + hidden: vec![Target::bare(hwnd, std::process::id())], frozen: vec![], muted: vec![], enhanced: false, + ..Default::default() }, ) .unwrap(); @@ -113,3 +116,62 @@ fn agent_restores_hidden_windows_left_by_a_crash() { } window_thread.join().unwrap(); } + +/// 跨重启的快照里 HWND 与 PID 都已失效,核心不得执行恢复动作,只清除文件。 +#[test] +fn agent_discards_snapshot_from_a_previous_boot() { + let (hwnd, window_tid, window_thread) = spawn_hidden_window(); + assert!(!is_visible(hwnd), "测试前提:窗口已隐藏"); + + let dir = tempfile::tempdir().unwrap(); + let config_path = dir.path().join("config.json"); + bosskey_common::Config::default() + .save(&config_path) + .unwrap(); + + // 手工构造「上一次开机」留下的快照:boot_time_ms 远早于本次开机。 + let recovery_path = dir.path().join(recovery::RECOVERY_FILE_NAME); + let stale = Snapshot { + schema: recovery::SCHEMA_CURRENT, + boot_time_ms: recovery::current_boot_time_ms() - 86_400_000, + hidden: vec![Target::bare(hwnd, std::process::id())], + frozen: vec![], + muted: vec![], + enhanced: false, + }; + std::fs::write(&recovery_path, serde_json::to_string(&stale).unwrap()).unwrap(); + + let pipe = r"\\.\pipe\bosskey_test_agent_recovery_stale"; + let options = AgentOptions { + config_path, + pipe_name: pipe.to_string(), + enable_tray: false, + auto_quit_ms: Some(15_000), + }; + let agent_thread = std::thread::spawn(move || agent::run(options)); + + let client = PipeClient::new(pipe); + let state = client.send(&Command::GetState).unwrap(); + assert_eq!(state, Response::State { hidden: false }); + + assert!( + !is_visible(hwnd), + "跨重启快照中的句柄不可信,不得执行恢复动作" + ); + assert!(!recovery_path.exists(), "过期快照应被清除,不再反复触发"); + + let quit = client.send(&Command::Quit).unwrap(); + assert_eq!(quit, Response::Ok); + + let deadline = Instant::now() + Duration::from_secs(10); + while !agent_thread.is_finished() { + assert!(Instant::now() < deadline, "代理线程未在退出命令后结束"); + std::thread::sleep(Duration::from_millis(50)); + } + agent_thread.join().unwrap(); + + unsafe { + let _ = PostThreadMessageW(window_tid, WM_QUIT, WPARAM(0), LPARAM(0)); + } + window_thread.join().unwrap(); +} diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index 9d9e3e1..597ffce 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -79,8 +79,10 @@ Boss-Key/ │ platform/win32.rs 窗口枚举/隐藏/显示(WindowManager trait) │ agent.rs 消息循环,聚合热键/托盘/IPC/鼠标/定时器 │ hotkey.rs 热键字符串 → RegisterHotKey 解析 -│ hide.rs 隐藏选择逻辑 + HideController(隐藏/显示编排) +│ hide.rs 隐藏选择逻辑 + HideController(plan/commit 两段式编排, +│ 含死句柄剪枝与恢复前的窗口/进程身份校验) │ effects.rs Effects trait(静音/冻结/暂停键,可注入 mock) +│ effects_worker.rs 副作用专职线程(FIFO 队列;消息循环只做 SW_HIDE) │ audio.rs Core Audio 会话静音 │ freeze.rs NtSuspend/Resume + pssuspend64 增强冻结 │ mouse_hook.rs WH_MOUSE_LL(中键/侧键/四角) @@ -92,7 +94,8 @@ Boss-Key/ │ elevation.rs 管理员检测 + UAC 提权重启 │ i18n.rs 核心用户可见文案 catalog(托盘菜单 / 气泡 / IPC 错误;日志不走它) │ logging.rs 分级文件日志(logs/BossKey-YYYY-MM-DD.log 按天切割 + panic 钩子) -│ recovery.rs 崩溃恢复(隐藏状态落盘,异常退出后找回窗口) +│ recovery.rs 崩溃恢复(意图先行落盘 + 原子写;快照带开机时刻与 +│ 进程创建时刻,跨重启的快照会被丢弃) │ icon.rs 进程图标提取(HICON → 手写 PNG/base64 编码) │ single_instance.rs 命名互斥单实例 └── apps/config/ 配置界面(Tauri 2 + Svelte 5) @@ -119,7 +122,9 @@ Boss-Key/ - 定时器(空闲检测、状态维护等); - 托盘图标交互。 -当触发隐藏 / 显示时,交由 `HideController` 编排:选择命中的窗口 → 应用 `Effects`(静音 / 冻结 / 暂停键)→ 隐藏 / 显示窗口,并把状态写入 `recovery.json`。 +当触发隐藏 / 显示时,交由 `HideController` 编排,流程为「意图先行」两段式:`plan_hide` 算出执行计划(剪掉失效记录、补齐 PID)→ 把计划后的快照写入 `recovery.json`(先落盘再动手,隐藏中途崩溃不丢记录)→ `commit_hide` 同步隐藏窗口(`SW_HIDE`),并把静音 / 冻结 / 暂停键交给副作用专职线程(`effects_worker.rs`)按 FIFO 异步执行——消息循环不被慢操作(音频枚举、pssuspend 等待)阻塞,热键与界面保持响应。 + +恢复(显示)时逐条校验记录的有效性:句柄须仍存在且仍属于当初的进程(`IsWindow` + PID 比对),冻结 / 静音记录须匹配进程创建时刻——句柄与 PID 都会被系统回收复用,校验不过的记录跳过并如实计入日志。 ::: info 可测试性设计 `Effects` 被抽象为 trait,测试时可注入 mock,从而在不真正静音 / 冻结系统的情况下验证隐藏编排逻辑。同理 `WindowManager` 也是 trait。 @@ -128,7 +133,7 @@ Boss-Key/ ## 稳定性设计(崩溃自愈三层防线) 1. **崩溃日志**:关键事件与 panic 写入 exe 同目录的 `logs/BossKey-YYYY-MM-DD.log`(按天切割,按 `log_retention_days` 保留,0 表示关闭日志;release 构建丢弃 DEBUG 级)。 -2. **崩溃恢复**:隐藏时把"隐藏 / 冻结 / 静音了什么"写入 `recovery.json`,异常退出后重启自动找回。 +2. **崩溃恢复**:隐藏动作执行前先把"将要隐藏 / 冻结 / 静音什么"写入 `recovery.json`(tmp + rename 原子替换),异常退出后重启自动找回;快照带开机时刻与进程创建时刻,跨重启的过期快照直接丢弃,不会对无关窗口 / 进程做恢复动作。 3. **看门狗**:计划任务 `RestartOnFailure`(崩溃后 1 分钟内重启,最多 3 次)。release 构建 `panic = "abort"`,panic 钩子写完日志后以非零码退出,正好触发计划任务重启。 用户视角的说明见 [窗口恢复与崩溃自愈](/guide/recovery)。 diff --git a/docs/en/dev/architecture.md b/docs/en/dev/architecture.md index 461fd7a..f13434d 100644 --- a/docs/en/dev/architecture.md +++ b/docs/en/dev/architecture.md @@ -79,8 +79,10 @@ Boss-Key/ │ platform/win32.rs Window enumeration/hiding/showing (WindowManager trait) │ agent.rs Message loop; aggregates hotkeys/tray/IPC/mouse/timers │ hotkey.rs Hotkey string → RegisterHotKey parsing -│ hide.rs Hiding selection logic + HideController (hide/show orchestration) +│ hide.rs Hiding selection logic + HideController (plan/commit two-phase orchestration +│ with stale-handle pruning and window/process identity checks before restore) │ effects.rs Effects trait (muting/freezing/pause key; mockable) +│ effects_worker.rs Dedicated side-effect thread (FIFO queue; the message loop only does SW_HIDE) │ audio.rs Core Audio session muting │ freeze.rs NtSuspend/Resume + pssuspend64 enhanced freezing │ mouse_hook.rs WH_MOUSE_LL (middle/side buttons, corners) @@ -92,7 +94,8 @@ Boss-Key/ │ elevation.rs Administrator detection + UAC elevation restart │ i18n.rs Catalog of user-visible core strings (tray menu / balloons / IPC errors; logs excluded) │ logging.rs Levelled file logging (logs/BossKey-YYYY-MM-DD.log, rotated daily + panic hook) -│ recovery.rs Crash recovery (hidden state persisted; windows recovered after an abnormal exit) +│ recovery.rs Crash recovery (intent persisted before acting + atomic writes; snapshots carry +│ boot time and process creation times, snapshots from a previous boot are discarded) │ icon.rs Process icon extraction (HICON → hand-written PNG/base64 encoding) │ single_instance.rs Named-mutex single instance └── apps/config/ Settings window (Tauri 2 + Svelte 5) @@ -119,7 +122,9 @@ Boss-Key/ - Timers (idle detection, state maintenance, and so on); - Tray icon interaction. -When hiding or showing is triggered, `HideController` orchestrates it: select the matching windows → apply `Effects` (muting / freezing / pause key) → hide or show the windows, and write the state to `recovery.json`. +When hiding or showing is triggered, `HideController` orchestrates it with a two-phase, intent-first flow: `plan_hide` computes the execution plan (pruning stale records and backfilling PIDs) → the planned snapshot is written to `recovery.json` (persist first, act second — a crash mid-hide loses no records) → `commit_hide` hides the windows synchronously (`SW_HIDE`) and hands muting / freezing / the pause key to the dedicated side-effect thread (`effects_worker.rs`), executed asynchronously in FIFO order — the message loop is never blocked by slow operations (audio enumeration, waiting on pssuspend), so hotkeys and the UI stay responsive. + +When restoring (showing), every record is validated first: the handle must still exist and still belong to the original process (`IsWindow` + PID comparison), and frozen / muted records must match the process creation time — both handles and PIDs are recycled by the system, and records that fail validation are skipped and reported truthfully in the log. ::: info Designed for testability `Effects` is a trait, so tests can inject a mock and verify the hiding orchestration without actually muting or freezing the system. `WindowManager` is a trait for the same reason. @@ -128,7 +133,7 @@ When hiding or showing is triggered, `HideController` orchestrates it: select th ## Stability (three layers of crash self-healing) 1. **Crash logs**: key events and panics are written to `logs/BossKey-YYYY-MM-DD.log` next to the exe (rotated daily, retained per `log_retention_days`; 0 disables logging; release builds drop the DEBUG level). -2. **Crash recovery**: hiding writes what was hidden / frozen / muted into `recovery.json`, recovered automatically on the next start after an abnormal exit. +2. **Crash recovery**: before any hide action executes, what is *about to be* hidden / frozen / muted is written to `recovery.json` (tmp + rename atomic replace); windows are recovered automatically on the next start after an abnormal exit. Snapshots carry the boot time and process creation times, so stale snapshots from a previous boot are discarded instead of acting on unrelated windows / processes. 3. **Watchdog**: the scheduled task's `RestartOnFailure` (restart within a minute of a crash, up to 3 times). Release builds use `panic = "abort"`, and the panic hook exits with a non-zero code once the log is written — exactly what triggers the scheduled-task restart. For the user-facing explanation see [Window recovery & crash self-healing](/en/guide/recovery). diff --git a/docs/zh-tw/dev/architecture.md b/docs/zh-tw/dev/architecture.md index c929166..24af136 100644 --- a/docs/zh-tw/dev/architecture.md +++ b/docs/zh-tw/dev/architecture.md @@ -79,8 +79,10 @@ Boss-Key/ │ platform/win32.rs 視窗列舉/隱藏/顯示(WindowManager trait) │ agent.rs 訊息迴圈,彙整快速鍵/通知區域/IPC/滑鼠/計時器 │ hotkey.rs 快速鍵字串 → RegisterHotKey 解析 -│ hide.rs 隱藏選擇邏輯 + HideController(隱藏/顯示編排) +│ hide.rs 隱藏選擇邏輯 + HideController(plan/commit 兩段式編排, +│ 含死控制代碼剪枝與復原前的視窗/處理程序身分校驗) │ effects.rs Effects trait(靜音/凍結/暫停鍵,可注入 mock) +│ effects_worker.rs 副作用專職執行緒(FIFO 佇列;訊息迴圈只做 SW_HIDE) │ audio.rs Core Audio 工作階段靜音 │ freeze.rs NtSuspend/Resume + pssuspend64 增強凍結 │ mouse_hook.rs WH_MOUSE_LL(中鍵/側鍵/四角) @@ -92,7 +94,8 @@ Boss-Key/ │ elevation.rs 系統管理員偵測 + UAC 提升權限重新啟動 │ i18n.rs 核心使用者可見文案 catalog(通知區域選單/通知/IPC 錯誤;記錄檔不走它) │ logging.rs 分級檔案記錄(logs/BossKey-YYYY-MM-DD.log 按日切割 + panic 掛鉤) -│ recovery.rs 當機復原(隱藏狀態寫入磁碟,異常結束後找回視窗) +│ recovery.rs 當機復原(意圖先行寫入 + 原子寫;快照帶開機時刻與 +│ 處理程序建立時刻,跨重新開機的快照會被丟棄) │ icon.rs 程序圖示擷取(HICON → 手寫 PNG/base64 編碼) │ single_instance.rs 具名互斥鎖單一執行個體 └── apps/config/ 設定介面(Tauri 2 + Svelte 5) @@ -119,7 +122,9 @@ Boss-Key/ - 計時器(閒置偵測、狀態維護等); - 通知區域圖示互動。 -當觸發隱藏/顯示時,交由 `HideController` 編排:選擇命中的視窗 → 套用 `Effects`(靜音/凍結/暫停鍵)→ 隱藏/顯示視窗,並把狀態寫入 `recovery.json`。 +當觸發隱藏/顯示時,交由 `HideController` 編排,流程為「意圖先行」兩段式:`plan_hide` 算出執行計畫(剪掉失效紀錄、補齊 PID)→ 把計畫後的快照寫入 `recovery.json`(先寫入再動手,隱藏中途當機不丟紀錄)→ `commit_hide` 同步隱藏視窗(`SW_HIDE`),並把靜音/凍結/暫停鍵交給副作用專職執行緒(`effects_worker.rs`)按 FIFO 非同步執行——訊息迴圈不被慢操作(音訊列舉、pssuspend 等待)阻塞,快速鍵與介面保持回應。 + +復原(顯示)時逐條校驗紀錄的有效性:控制代碼須仍存在且仍屬於當初的處理程序(`IsWindow` + PID 比對),凍結/靜音紀錄須符合處理程序建立時刻——控制代碼與 PID 都會被系統回收重複使用,校驗不過的紀錄跳過並如實計入日誌。 ::: info 可測試性設計 `Effects` 被抽象為 trait,測試時可注入 mock,從而在不真正靜音/凍結系統的情況下驗證隱藏編排邏輯。同理 `WindowManager` 也是 trait。 @@ -128,7 +133,7 @@ Boss-Key/ ## 穩定性設計(當機自癒三層防線) 1. **當機記錄**:關鍵事件與 panic 寫入 exe 同資料夾的 `logs/BossKey-YYYY-MM-DD.log`(按日切割,依 `log_retention_days` 保留,0 表示不記錄;release 建置丟棄 DEBUG 級)。 -2. **當機復原**:隱藏時把「隱藏/凍結/靜音了什麼」寫入 `recovery.json`,異常結束後重新啟動自動找回。 +2. **當機復原**:隱藏動作執行前先把「將要隱藏/凍結/靜音什麼」寫入 `recovery.json`(tmp + rename 原子替換),異常結束後重新啟動自動找回;快照帶開機時刻與處理程序建立時刻,跨重新開機的過期快照直接丟棄,不會對無關視窗/處理程序做復原動作。 3. **監控程式**:排程工作 `RestartOnFailure`(當機後 1 分鐘內重新啟動,最多 3 次)。release 建置 `panic = "abort"`,panic 掛鉤寫完記錄後以非零碼結束,正好觸發排程工作重新啟動。 使用者視角的說明見 [視窗復原與當機自癒](/zh-tw/guide/recovery)。 From 25327df39b861fbe2776e0e7dab3baa2392589fb Mon Sep 17 00:00:00 2001 From: Ivan Hanloth Date: Sun, 26 Jul 2026 14:34:04 +0100 Subject: [PATCH 14/19] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96IPC=E9=80=9A?= =?UTF-8?q?=E4=BF=A1=E7=A8=B3=E5=AE=9A=E6=80=A7=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E6=B6=88=E6=81=AF=E5=BE=AA=E7=8E=AF=EF=BC=8C?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=B7=A5=E5=85=B7=E4=B8=8E=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/config/src-tauri/src/lib.rs | 25 +++- crates/common/src/ipc.rs | 15 +++ crates/core/src/agent.rs | 136 +++++++++++++++------- crates/core/src/hide.rs | 58 +++++++++ crates/core/src/ipc_server.rs | 33 +++++- crates/core/src/keyboard_hook.rs | 11 +- crates/core/tests/agent_tool_alignment.rs | 116 ++++++++++++++++++ docs/dev/architecture.md | 4 +- docs/dev/ipc-protocol.md | 2 + docs/en/dev/architecture.md | 4 +- docs/en/dev/ipc-protocol.md | 2 + docs/en/guide/recovery.md | 2 + docs/guide/recovery.md | 2 + docs/zh-tw/dev/architecture.md | 4 +- docs/zh-tw/dev/ipc-protocol.md | 2 + docs/zh-tw/guide/recovery.md | 2 + 16 files changed, 364 insertions(+), 54 deletions(-) create mode 100644 crates/core/tests/agent_tool_alignment.rs diff --git a/apps/config/src-tauri/src/lib.rs b/apps/config/src-tauri/src/lib.rs index 8e775a3..4fbb4f7 100644 --- a/apps/config/src-tauri/src/lib.rs +++ b/apps/config/src-tauri/src/lib.rs @@ -124,10 +124,25 @@ async fn show_all_windows() -> Result<(), String> { blocking(|| notify_core(&Command::Show).map(|_| ())).await } -/// 恢复显示指定窗口(窗口恢复工具):直接对选中的句柄 ShowWindow。 +/// 把恢复工具的窗口操作交给核心执行;核心未应答(未运行 / 出错)时返回 false, +/// 由调用方退回直接操作。 +fn try_core_window_op(command: &Command) -> bool { + matches!( + PipeClient::connect_default().fast().send(command), + Ok(Response::Ok) + ) +} + +/// 恢复显示指定窗口(窗口恢复工具)。优先经核心释放并更新记录; +/// 核心不在运行才直接对句柄 ShowWindow。 #[tauri::command] async fn show_windows(hwnds: Vec) { blocking(move || { + if try_core_window_op(&Command::ReleaseWindows { + hwnds: hwnds.clone(), + }) { + return; + } use bosskey_core::platform::WindowManager; let mgr = bosskey_core::platform::manager(); for h in hwnds { @@ -137,10 +152,16 @@ async fn show_windows(hwnds: Vec) { .await } -/// 隐藏指定窗口(窗口恢复工具):直接对选中的句柄隐藏。 +/// 隐藏指定窗口(窗口恢复工具)。优先经核心纳入隐藏记录(不施加静音 / 冻结); +/// 核心不在运行才直接对句柄隐藏。 #[tauri::command] async fn hide_windows(hwnds: Vec) { blocking(move || { + if try_core_window_op(&Command::AdoptWindows { + hwnds: hwnds.clone(), + }) { + return; + } use bosskey_core::platform::WindowManager; let mgr = bosskey_core::platform::manager(); for h in hwnds { diff --git a/crates/common/src/ipc.rs b/crates/common/src/ipc.rs index 3b58667..b238d5b 100644 --- a/crates/common/src/ipc.rs +++ b/crates/common/src/ipc.rs @@ -27,6 +27,16 @@ pub enum Command { SetHotkeys { enabled: bool, }, + /// 窗口恢复工具:恢复显示指定句柄。在核心隐藏记录里的窗口按整进程释放 + /// (连同解冻 / 取消静音);不在记录里的句柄直接恢复显示。 + ReleaseWindows { + hwnds: Vec, + }, + /// 窗口恢复工具:隐藏指定句柄并纳入核心隐藏记录(享受崩溃恢复保护), + /// 不施加静音 / 冻结 / 暂停键。 + AdoptWindows { + hwnds: Vec, + }, Quit, } @@ -164,6 +174,11 @@ mod tests { }, Command::SetHotkeys { enabled: true }, Command::SetHotkeys { enabled: false }, + Command::ReleaseWindows { + hwnds: vec![1, 2, 3], + }, + Command::ReleaseWindows { hwnds: vec![] }, + Command::AdoptWindows { hwnds: vec![42] }, Command::Quit, ]; for c in cases { diff --git a/crates/core/src/agent.rs b/crates/core/src/agent.rs index d92b0f8..daa6b4f 100644 --- a/crates/core/src/agent.rs +++ b/crates/core/src/agent.rs @@ -1,9 +1,10 @@ +use std::cell::RefCell; use std::path::{Path, PathBuf}; use std::sync::mpsc::{Receiver, Sender, channel}; use std::time::Duration; use bosskey_common::ipc::{Command, Response}; -use bosskey_common::{APP_NAME, ARG_ABOUT, ARG_RESTORE, Config}; +use bosskey_common::{APP_NAME, ARG_ABOUT, ARG_RESTORE, Config, Setting}; use windows::Win32::Foundation::{ERROR_HOTKEY_ALREADY_REGISTERED, HWND, LPARAM, LRESULT, WPARAM}; use windows::Win32::System::LibraryLoader::GetModuleHandleW; use windows::Win32::UI::Input::KeyboardAndMouse::{ @@ -312,16 +313,26 @@ impl AgentState { /// 意图先行:先落盘计划后的快照,再隐藏窗口;副作用由专职线程异步执行。 fn hide_with_plan(&mut self, targets: &[crate::hide::Target], freeze_set: &[u32]) -> HidePlan { - let plan = self - .controller - .plan_hide(&self.config.setting, targets, freeze_set); + let setting = self.config.setting.clone(); + let plan = self.hide_with_plan_using(&setting, targets, freeze_set); + if self.config.notifications.on_hide && !plan.fresh.is_empty() { + self.balloon(APP_NAME, i18n::t(Msg::HiddenBody)); + } + plan + } + + /// [`Self::hide_with_plan`] 的按指定设置版本(恢复工具的手动隐藏关闭副作用)。 + fn hide_with_plan_using( + &mut self, + setting: &Setting, + targets: &[crate::hide::Target], + freeze_set: &[u32], + ) -> HidePlan { + let plan = self.controller.plan_hide(setting, targets, freeze_set); let planned = self.controller.planned_snapshot(&plan); self.persist_snapshot(&planned); self.controller.commit_hide(plan.clone()); self.sync_tray(); - if self.config.notifications.on_hide && !plan.fresh.is_empty() { - self.balloon(APP_NAME, i18n::t(Msg::HiddenBody)); - } plan } @@ -464,6 +475,29 @@ impl AgentState { } (Response::Ok, false) } + Command::ReleaseWindows { hwnds } => { + let released = self.controller.release_windows(&hwnds); + if released > 0 { + logging::info(&format!("窗口恢复工具释放 {released} 个窗口")); + self.persist_recovery(); + self.sync_tray(); + } + (Response::Ok, false) + } + Command::AdoptWindows { hwnds } => { + let targets: Vec = + hwnds.iter().map(|&h| crate::hide::Target::bare(h, 0)).collect(); + // 恢复工具的手动隐藏不施加副作用,仅隐藏并纳入记录。 + let mut setting = self.config.setting.clone(); + setting.mute_after_hide = false; + setting.freeze_after_hide = false; + setting.send_before_hide = false; + let plan = self.hide_with_plan_using(&setting, &targets, &[]); + if !plan.fresh.is_empty() { + logging::info(&format!("窗口恢复工具隐藏 {} 个窗口", plan.fresh.len())); + } + (Response::Ok, false) + } Command::Quit => (Response::Ok, true), } } @@ -621,10 +655,12 @@ fn toggle_autostart(state: &AgentState) { } } -fn state_mut<'a>(hwnd: HWND) -> Option<&'a mut AgentState> { +/// 代理窗口 `GWLP_USERDATA` 里存放的状态单元。用 `RefCell` 而非裸 `&mut`: +/// 菜单模态循环会重入 `wndproc`,重入时借用失败、事件被安全丢弃。 +fn state_cell<'a>(hwnd: HWND) -> Option<&'a RefCell> { unsafe { - let ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut AgentState; - ptr.as_mut() + let ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *const RefCell; + ptr.as_ref() } } @@ -769,9 +805,14 @@ fn quit(state: &mut AgentState, hwnd: HWND) { } unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { - let Some(state) = state_mut(hwnd) else { + let Some(cell) = state_cell(hwnd) else { + return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }; + }; + // 借用失败即模态菜单重入:丢弃本次事件。 + let Ok(mut state) = cell.try_borrow_mut() else { return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }; }; + let state = &mut *state; if msg != 0 && msg == crate::tray::taskbar_created_msg() { if let Some(tray) = &mut state.tray { tray.on_taskbar_created(); @@ -817,6 +858,10 @@ unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: state.controller.is_hidden(), state.config.setting.auto_hide_enabled, ); + // 补发 IPC 唤醒,排干菜单模态期间积压的命令。 + unsafe { + let _ = PostMessageW(Some(hwnd), WM_APP_IPC, WPARAM(0), LPARAM(0)); + } } _ => {} } @@ -833,6 +878,9 @@ unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: // 右键菜单以代理窗口为宿主,经 WM_COMMAND 复用处理。 FLOAT_MENU => { crate::float_window::show_float_menu(hwnd, MENU_SETTINGS, MENU_QUIT); + unsafe { + let _ = PostMessageW(Some(hwnd), WM_APP_IPC, WPARAM(0), LPARAM(0)); + } } _ => {} } @@ -1056,7 +1104,7 @@ pub fn run(options: AgentOptions) { // 副作用由专职线程执行,消息循环不被慢操作阻塞。 let effects_worker = EffectsWorker::spawn(WinEffects::new(exe_dir)); - let mut state = Box::new(AgentState { + let state = Box::new(RefCell::new(AgentState { config, config_path: options.config_path.clone(), recovery_path, @@ -1071,37 +1119,44 @@ pub fn run(options: AgentOptions) { monitoring: true, hotkeys_armed: false, keyboard_hook: None, - }); + })); // 恢复文件存在即上次异常退出仍有窗口被隐藏,先找回。 - if let Some(snapshot) = recovery::load(&state.recovery_path) { - if snapshot.is_restorable(recovery::current_boot_time_ms()) { - logging::warn(&format!( - "检测到上次异常退出,开始找回 {} 个被隐藏的窗口(另需解冻 {} 个、取消静音 {} 个进程)", - snapshot.hidden.len(), - snapshot.frozen.len(), - snapshot.muted.len() - )); - let outcome = state.controller.restore_from(snapshot); - logging::info(&format!( - "崩溃恢复完成:恢复 {} 个窗口,跳过 {} 条失效记录", - outcome.shown, outcome.stale - )); - } else { - // 跨重启(或旧版格式)快照中的句柄与 PID 已失效,只丢弃并留档。 - log_warn!( - "恢复文件来自上一次开机或旧版本,其中的窗口句柄已失效,跳过恢复: {}", - state.recovery_path.display() - ); + { + let mut state = state.borrow_mut(); + if let Some(snapshot) = recovery::load(&state.recovery_path) { + if snapshot.is_restorable(recovery::current_boot_time_ms()) { + logging::warn(&format!( + "检测到上次异常退出,开始找回 {} 个被隐藏的窗口(另需解冻 {} 个、取消静音 {} 个进程)", + snapshot.hidden.len(), + snapshot.frozen.len(), + snapshot.muted.len() + )); + let outcome = state.controller.restore_from(snapshot); + logging::info(&format!( + "崩溃恢复完成:恢复 {} 个窗口,跳过 {} 条失效记录", + outcome.shown, outcome.stale + )); + } else { + // 跨重启(或旧版格式)快照中的句柄与 PID 已失效,只丢弃并留档。 + log_warn!( + "恢复文件来自上一次开机或旧版本,其中的窗口句柄已失效,跳过恢复: {}", + state.recovery_path.display() + ); + } + recovery::clear(&state.recovery_path); } - recovery::clear(&state.recovery_path); } unsafe { - SetWindowLongPtrW(hwnd, GWLP_USERDATA, &mut *state as *mut AgentState as isize); + SetWindowLongPtrW( + hwnd, + GWLP_USERDATA, + &*state as *const RefCell as isize, + ); } - state.sync_monitoring(hwnd); + state.borrow_mut().sync_monitoring(hwnd); let hwnd_value = hwnd.0 as isize; ipc_server::spawn(options.pipe_name.clone(), move |cmd| { @@ -1126,10 +1181,13 @@ pub fn run(options: AgentOptions) { }) }); - if state.config.notifications.on_start - && let Some(tray) = &state.tray { - tray.balloon(i18n::t(Msg::StartTitle), i18n::t(Msg::StartBody)); + let state = state.borrow(); + if state.config.notifications.on_start + && let Some(tray) = &state.tray + { + tray.balloon(i18n::t(Msg::StartTitle), i18n::t(Msg::StartBody)); + } } if let Some(ms) = options.auto_quit_ms { @@ -1154,7 +1212,7 @@ pub fn run(options: AgentOptions) { } // 非 quit() 路径(如 WM_DESTROY)退出时兜底排干副作用队列。 - if let Some(worker) = state.effects_worker.take() { + if let Some(worker) = state.borrow_mut().effects_worker.take() { worker.shutdown(EFFECTS_SHUTDOWN_TIMEOUT); } diff --git a/crates/core/src/hide.rs b/crates/core/src/hide.rs index de8bd0c..acf6e4f 100644 --- a/crates/core/src/hide.rs +++ b/crates/core/src/hide.rs @@ -458,6 +458,28 @@ impl HideController { show.len() } + /// 恢复显示指定句柄(窗口恢复工具经 IPC 调用)。在隐藏记录里的窗口按 + /// 整进程释放(连同解冻 / 取消静音);不在记录里的句柄直接恢复显示。 + /// 返回从记录中释放的窗口数。 + pub fn release_windows(&mut self, hwnds: &[i64]) -> usize { + let known: HashSet = self.hidden.iter().map(|t| t.hwnd).collect(); + let mut pids: Vec = self + .hidden + .iter() + .filter(|t| hwnds.contains(&t.hwnd)) + .map(|t| t.pid) + .collect(); + pids.sort_unstable(); + pids.dedup(); + let released = self.release_pids(&pids); + for &hwnd in hwnds { + if !known.contains(&hwnd) && self.wm.is_window(hwnd) { + self.wm.show(hwnd); + } + } + released + } + /// 当前隐藏状态的快照,用于崩溃恢复落盘。版本与开机时刻由 `recovery::save` 盖章。 pub fn snapshot(&self) -> Snapshot { Snapshot { @@ -1227,6 +1249,42 @@ mod tests { ); } + #[test] + fn release_windows_frees_whole_process_and_shows_unknown_handles() { + let setting = Setting { + hide_current: false, + mute_after_hide: true, + freeze_after_hide: true, + ..Setting::default() + }; + let wm = MockWm::new( + vec![ + win_pid("主窗口", 10, "game.exe", 500, "C:\\game.exe"), + win_pid("子窗口", 11, "game.exe", 500, "C:\\game.exe"), + win_pid("无关窗口", 30, "other.exe", 700, "C:\\other.exe"), + ], + 0, + ); + // 记录外的句柄:被别的途径隐藏,核心不知情。 + wm.hide(30); + let mut controller = HideController::new(wm, MockEffects::default()); + controller.apply_hide( + &setting, + &[Target::bare(10, 500), Target::bare(11, 500)], + &[500], + ); + + // 只点选了一个窗口 + 一个记录外句柄。 + assert_eq!(controller.release_windows(&[10, 30]), 2); + assert!( + controller.wm.is_visible(10) && controller.wm.is_visible(11), + "同进程的两个窗口应一起释放,避免解冻后仍有窗口藏着" + ); + assert_eq!(*controller.effects.resumes.borrow(), vec![500], "应解冻"); + assert!(controller.wm.is_visible(30), "记录外的句柄应直接恢复显示"); + assert!(!controller.is_hidden()); + } + #[test] fn expand_descendants_collects_multi_level_tree() { // 1 → 2 → 4, 1 → 3;另有无关的 9 → 10。 diff --git a/crates/core/src/ipc_server.rs b/crates/core/src/ipc_server.rs index 63a408d..5964abc 100644 --- a/crates/core/src/ipc_server.rs +++ b/crates/core/src/ipc_server.rs @@ -14,10 +14,22 @@ use windows::Win32::System::Pipes::{ }; use windows::core::{HRESULT, PCWSTR}; +use crate::log_error; use crate::util::to_wide_null; const PIPE_BUF_SIZE: u32 = 4096; +/// 创建管道失败后的重试间隔:逐级退避,最后一档一直沿用,线程不退出。 +const RETRY_DELAYS: [std::time::Duration; 3] = [ + std::time::Duration::from_secs(1), + std::time::Duration::from_secs(5), + std::time::Duration::from_secs(30), +]; + +fn retry_delay(attempt: u32) -> std::time::Duration { + RETRY_DELAYS[(attempt as usize).min(RETRY_DELAYS.len() - 1)] +} + /// 命名管道安全描述符(SDDL):Everyone 可读写,完整性标签设为 Low, /// 使普通配置程序能连上以管理员运行的核心。 const PIPE_SDDL: &str = "D:(A;;GRGW;;;WD)S:(ML;;NW;;;LW)"; @@ -95,6 +107,7 @@ where crate::logging::warn("命名管道安全描述符构造失败,管理员核心下普通配置程序可能无法连接"); } let sa_ptr = security.as_ref().map(PipeSecurity::as_ptr); + let mut failures: u32 = 0; loop { let handle = unsafe { CreateNamedPipeW( @@ -109,9 +122,16 @@ where ) }; if handle == INVALID_HANDLE_VALUE { - eprintln!("创建命名管道失败: {pipe_name}"); - return; + let err = std::io::Error::last_os_error(); + let delay = retry_delay(failures); + failures += 1; + log_error!( + "创建命名管道失败(第 {failures} 次),{delay:?} 后重试: {pipe_name} — {err}" + ); + std::thread::sleep(delay); + continue; } + failures = 0; if !is_client_connected(unsafe { ConnectNamedPipe(handle, None) }) { unsafe { @@ -188,6 +208,15 @@ mod tests { buf.trim_end().to_string() } + #[test] + fn retry_delay_backs_off_and_caps_at_the_last_step() { + assert_eq!(retry_delay(0), Duration::from_secs(1)); + assert_eq!(retry_delay(1), Duration::from_secs(5)); + assert_eq!(retry_delay(2), Duration::from_secs(30)); + assert_eq!(retry_delay(3), Duration::from_secs(30)); + assert_eq!(retry_delay(1000), Duration::from_secs(30)); + } + #[test] fn pipe_security_descriptor_builds_from_sddl() { let sec = PipeSecurity::new().expect("SDDL 应能解析出安全描述符"); diff --git a/crates/core/src/keyboard_hook.rs b/crates/core/src/keyboard_hook.rs index 8b85742..ad46f79 100644 --- a/crates/core/src/keyboard_hook.rs +++ b/crates/core/src/keyboard_hook.rs @@ -118,16 +118,11 @@ unsafe extern "system" fn hook_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) let data = unsafe { &*(lparam.0 as *const KBDLLHOOKSTRUCT) }; // 注入的按键(自家的媒体键模拟等)不参与判定,避免反馈回路。 if data.flags.0 & LLKHF_INJECTED.0 == 0 { + // GetKeyState 在锁外调用,锁内只剩纯内存判定。 + let pressed = pressed_modifiers(); let decision = HOTKEYS .lock() - .map(|mut states| { - decide( - wparam.0 as u32, - data.vkCode as u16, - pressed_modifiers(), - &mut states, - ) - }) + .map(|mut states| decide(wparam.0 as u32, data.vkCode as u16, pressed, &mut states)) .unwrap_or(Decision::Pass); match decision { Decision::Pass => {} diff --git a/crates/core/tests/agent_tool_alignment.rs b/crates/core/tests/agent_tool_alignment.rs new file mode 100644 index 0000000..91113aa --- /dev/null +++ b/crates/core/tests/agent_tool_alignment.rs @@ -0,0 +1,116 @@ +//! 端到端:窗口恢复工具经 IPC 隐藏 / 释放窗口,核心记录与恢复文件同步更新。 + +use std::time::{Duration, Instant}; + +use bosskey_common::ipc::{Command, PipeClient, Response}; +use bosskey_core::agent::{self, AgentOptions}; +use bosskey_core::recovery; +use windows::Win32::Foundation::{HWND, LPARAM, WPARAM}; +use windows::Win32::System::Threading::GetCurrentThreadId; +use windows::Win32::UI::WindowsAndMessaging::{ + CW_USEDEFAULT, CreateWindowExW, DestroyWindow, DispatchMessageW, GetMessageW, IsWindowVisible, + MSG, PostThreadMessageW, SW_SHOW, ShowWindow, TranslateMessage, WINDOW_EX_STYLE, WM_QUIT, + WS_OVERLAPPEDWINDOW, +}; +use windows::core::w; + +/// 在带消息循环的独立线程上创建一个可见窗口。 +fn spawn_visible_window() -> (i64, u32, std::thread::JoinHandle<()>) { + let (tx, rx) = std::sync::mpsc::channel::<(i64, u32)>(); + let handle = std::thread::spawn(move || unsafe { + let hwnd = CreateWindowExW( + WINDOW_EX_STYLE(0), + w!("Static"), + w!("BossKeyToolAlignmentTestWindow"), + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + 200, + 120, + None, + None, + None, + None, + ) + .expect("创建测试窗口失败"); + let _ = ShowWindow(hwnd, SW_SHOW); + tx.send((hwnd.0 as isize as i64, GetCurrentThreadId())) + .unwrap(); + + let mut msg: MSG = std::mem::zeroed(); + while GetMessageW(&mut msg, None, 0, 0).0 > 0 { + let _ = TranslateMessage(&msg); + DispatchMessageW(&msg); + } + let _ = DestroyWindow(hwnd); + }); + let (hwnd, tid) = rx.recv().expect("窗口线程未上报句柄"); + (hwnd, tid, handle) +} + +fn is_visible(hwnd: i64) -> bool { + unsafe { IsWindowVisible(HWND(hwnd as isize as *mut std::ffi::c_void)).as_bool() } +} + +#[test] +fn adopt_and_release_keep_core_records_and_recovery_file_in_sync() { + let (hwnd, window_tid, window_thread) = spawn_visible_window(); + assert!(is_visible(hwnd), "测试前提:窗口可见"); + + let dir = tempfile::tempdir().unwrap(); + let config_path = dir.path().join("config.json"); + bosskey_common::Config::default() + .save(&config_path) + .unwrap(); + let recovery_path = dir.path().join(recovery::RECOVERY_FILE_NAME); + + let pipe = r"\\.\pipe\bosskey_test_tool_alignment"; + let options = AgentOptions { + config_path, + pipe_name: pipe.to_string(), + enable_tray: false, + auto_quit_ms: Some(15_000), + }; + let agent_thread = std::thread::spawn(move || agent::run(options)); + let client = PipeClient::new(pipe); + + // 恢复工具隐藏:窗口不可见、核心记录为隐藏态、恢复文件已写出。 + let reply = client + .send(&Command::AdoptWindows { hwnds: vec![hwnd] }) + .unwrap(); + assert_eq!(reply, Response::Ok); + assert!(!is_visible(hwnd), "经核心隐藏后窗口应不可见"); + assert_eq!( + client.send(&Command::GetState).unwrap(), + Response::State { hidden: true }, + "核心记录应包含工具隐藏的窗口" + ); + assert!(recovery_path.exists(), "工具隐藏的窗口应受崩溃恢复保护"); + + // 恢复工具释放:窗口找回、记录清空、恢复文件删除。 + let reply = client + .send(&Command::ReleaseWindows { hwnds: vec![hwnd] }) + .unwrap(); + assert_eq!(reply, Response::Ok); + assert!(is_visible(hwnd), "释放后窗口应恢复可见"); + assert_eq!( + client.send(&Command::GetState).unwrap(), + Response::State { hidden: false } + ); + assert!(!recovery_path.exists(), "记录清空后恢复文件应删除"); + + let quit = client.send(&Command::Quit).unwrap(); + assert_eq!(quit, Response::Ok); + + let deadline = Instant::now() + Duration::from_secs(10); + while !agent_thread.is_finished() { + assert!(Instant::now() < deadline, "代理线程未在退出命令后结束"); + std::thread::sleep(Duration::from_millis(50)); + } + agent_thread.join().unwrap(); + + unsafe { + let _ = PostThreadMessageW(window_tid, WM_QUIT, WPARAM(0), LPARAM(0)); + } + window_thread.join().unwrap(); +} diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index 597ffce..0793809 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -89,7 +89,7 @@ Boss-Key/ │ keyboard_hook.rs WH_KEYBOARD_LL(「不传递」热键拦截) │ idle.rs GetLastInputInfo 空闲 + 自动隐藏判定 │ tray.rs Shell_NotifyIcon 托盘 + 气泡 -│ ipc_server.rs 命名管道服务端 +│ ipc_server.rs 命名管道服务端(创建失败退避重试,不退出) │ autostart.rs 开机自启(计划任务 XML 含失败自动重启 + 注册表回退) │ elevation.rs 管理员检测 + UAC 提权重启 │ i18n.rs 核心用户可见文案 catalog(托盘菜单 / 气泡 / IPC 错误;日志不走它) @@ -122,6 +122,8 @@ Boss-Key/ - 定时器(空闲检测、状态维护等); - 托盘图标交互。 +消息循环状态由 `RefCell` 承载:托盘 / 悬浮窗菜单的模态循环(`TrackPopupMenu`)会重入 `wndproc`,重入期间的事件借用失败即被安全丢弃,避免出现两个可变引用的别名。IPC 线程创建命名管道失败时按退避(1s → 5s → 30s)重试,不会退出。 + 当触发隐藏 / 显示时,交由 `HideController` 编排,流程为「意图先行」两段式:`plan_hide` 算出执行计划(剪掉失效记录、补齐 PID)→ 把计划后的快照写入 `recovery.json`(先落盘再动手,隐藏中途崩溃不丢记录)→ `commit_hide` 同步隐藏窗口(`SW_HIDE`),并把静音 / 冻结 / 暂停键交给副作用专职线程(`effects_worker.rs`)按 FIFO 异步执行——消息循环不被慢操作(音频枚举、pssuspend 等待)阻塞,热键与界面保持响应。 恢复(显示)时逐条校验记录的有效性:句柄须仍存在且仍属于当初的进程(`IsWindow` + PID 比对),冻结 / 静音记录须匹配进程创建时刻——句柄与 PID 都会被系统回收复用,校验不过的记录跳过并如实计入日志。 diff --git a/docs/dev/ipc-protocol.md b/docs/dev/ipc-protocol.md index c1c280b..52d1b9f 100644 --- a/docs/dev/ipc-protocol.md +++ b/docs/dev/ipc-protocol.md @@ -27,6 +27,8 @@ title: IPC 协议 | `Toggle` | `{"cmd":"toggle"}` | 切换隐藏 / 显示 | | `SetAutostart` | `{"cmd":"set_autostart","enabled":true}` | 设置开机自启 | | `SetHotkeys` | `{"cmd":"set_hotkeys","enabled":false}` | 临时停用 / 恢复热键与鼠标监控 | +| `ReleaseWindows` | `{"cmd":"release_windows","hwnds":[..]}` | 窗口恢复工具:恢复显示指定句柄。在核心记录里的窗口按整进程释放(连同解冻 / 取消静音);记录外的句柄直接显示 | +| `AdoptWindows` | `{"cmd":"adopt_windows","hwnds":[..]}` | 窗口恢复工具:隐藏指定句柄并纳入核心记录(享受崩溃恢复),不施加静音 / 冻结 | | `Quit` | `{"cmd":"quit"}` | 退出核心 | ## Response(核心 → 配置界面) diff --git a/docs/en/dev/architecture.md b/docs/en/dev/architecture.md index f13434d..ead110c 100644 --- a/docs/en/dev/architecture.md +++ b/docs/en/dev/architecture.md @@ -89,7 +89,7 @@ Boss-Key/ │ keyboard_hook.rs WH_KEYBOARD_LL ("don't pass through" hotkey interception) │ idle.rs GetLastInputInfo idle detection + auto-hide decision │ tray.rs Shell_NotifyIcon tray + balloons -│ ipc_server.rs Named-pipe server +│ ipc_server.rs Named-pipe server (retries pipe creation with backoff instead of exiting) │ autostart.rs Startup (scheduled-task XML with restart-on-failure + registry fallback) │ elevation.rs Administrator detection + UAC elevation restart │ i18n.rs Catalog of user-visible core strings (tray menu / balloons / IPC errors; logs excluded) @@ -122,6 +122,8 @@ Boss-Key/ - Timers (idle detection, state maintenance, and so on); - Tray icon interaction. +Message-loop state lives in a `RefCell`: the modal loops of the tray / floating-window menus (`TrackPopupMenu`) re-enter `wndproc`, and events arriving during re-entry fail the borrow and are safely dropped, so no aliased mutable references can exist. The IPC thread retries pipe creation with backoff (1s → 5s → 30s) instead of exiting. + When hiding or showing is triggered, `HideController` orchestrates it with a two-phase, intent-first flow: `plan_hide` computes the execution plan (pruning stale records and backfilling PIDs) → the planned snapshot is written to `recovery.json` (persist first, act second — a crash mid-hide loses no records) → `commit_hide` hides the windows synchronously (`SW_HIDE`) and hands muting / freezing / the pause key to the dedicated side-effect thread (`effects_worker.rs`), executed asynchronously in FIFO order — the message loop is never blocked by slow operations (audio enumeration, waiting on pssuspend), so hotkeys and the UI stay responsive. When restoring (showing), every record is validated first: the handle must still exist and still belong to the original process (`IsWindow` + PID comparison), and frozen / muted records must match the process creation time — both handles and PIDs are recycled by the system, and records that fail validation are skipped and reported truthfully in the log. diff --git a/docs/en/dev/ipc-protocol.md b/docs/en/dev/ipc-protocol.md index bf910e9..cdc76c6 100644 --- a/docs/en/dev/ipc-protocol.md +++ b/docs/en/dev/ipc-protocol.md @@ -27,6 +27,8 @@ Serialised with `#[serde(tag = "cmd", rename_all = "snake_case")]`. | `Toggle` | `{"cmd":"toggle"}` | Toggle hide / show | | `SetAutostart` | `{"cmd":"set_autostart","enabled":true}` | Configure startup | | `SetHotkeys` | `{"cmd":"set_hotkeys","enabled":false}` | Temporarily suspend / resume hotkey and mouse monitoring | +| `ReleaseWindows` | `{"cmd":"release_windows","hwnds":[..]}` | Recovery tool: show the given handles. Windows tracked by the core are released per whole process (including unfreeze / unmute); untracked handles are simply shown | +| `AdoptWindows` | `{"cmd":"adopt_windows","hwnds":[..]}` | Recovery tool: hide the given handles and track them in the core (covered by crash recovery), without muting / freezing | | `Quit` | `{"cmd":"quit"}` | Exit the core | ## Response (core → settings window) diff --git a/docs/en/guide/recovery.md b/docs/en/guide/recovery.md index ee2362e..4e177e3 100644 --- a/docs/en/guide/recovery.md +++ b/docs/en/guide/recovery.md @@ -18,6 +18,8 @@ The tool lists every window on the system (including currently invisible ones). The tool can also **freeze / resume** windows' processes. A frozen window stops rendering, which lowers resource usage. Frozen windows can still be recovered with the tool. +While the core is running, the tool's show / hide operations are carried out by the core: manually hidden windows are covered by crash recovery too, and restoring a window also unfreezes and unmutes its process. When the core is not running, the tool operates on windows directly. + ## Three layers of defence The Boss Key core has **three layers of crash self-healing**, so windows stay safe even if the program crashes. diff --git a/docs/guide/recovery.md b/docs/guide/recovery.md index e36aef7..864634b 100644 --- a/docs/guide/recovery.md +++ b/docs/guide/recovery.md @@ -18,6 +18,8 @@ title: 窗口恢复与崩溃防护 窗口恢复工具也支持将窗口**冻结 / 解冻**,冻结后窗口会被暂停渲染,降低资源消耗。冻结的窗口仍然可通过恢复工具找回。 +核心运行时,工具的显示 / 隐藏操作会交给核心执行:手动隐藏的窗口同样纳入崩溃恢复保护;恢复显示时会连同解冻、取消静音一起完成。核心未运行时工具直接操作窗口。 + ## 三层防线 Boss Key 核心内置了**崩溃自愈三层防线**,即使程序意外崩溃也能尽量保证窗口安全。 diff --git a/docs/zh-tw/dev/architecture.md b/docs/zh-tw/dev/architecture.md index 24af136..c119da5 100644 --- a/docs/zh-tw/dev/architecture.md +++ b/docs/zh-tw/dev/architecture.md @@ -89,7 +89,7 @@ Boss-Key/ │ keyboard_hook.rs WH_KEYBOARD_LL(「不傳遞」快速鍵攔截) │ idle.rs GetLastInputInfo 閒置 + 自動隱藏判定 │ tray.rs Shell_NotifyIcon 通知區域 + 通知 -│ ipc_server.rs 具名管道伺服端 +│ ipc_server.rs 具名管道伺服端(建立失敗退避重試,不結束) │ autostart.rs 開機自動啟動(排程工作 XML 含失敗自動重新啟動 + 登錄檔回落) │ elevation.rs 系統管理員偵測 + UAC 提升權限重新啟動 │ i18n.rs 核心使用者可見文案 catalog(通知區域選單/通知/IPC 錯誤;記錄檔不走它) @@ -122,6 +122,8 @@ Boss-Key/ - 計時器(閒置偵測、狀態維護等); - 通知區域圖示互動。 +訊息迴圈狀態由 `RefCell` 承載:通知區域/懸浮窗選單的強制回應迴圈(`TrackPopupMenu`)會重入 `wndproc`,重入期間的事件借用失敗即被安全丟棄,避免出現兩個可變參考的別名。IPC 執行緒建立具名管道失敗時按退避(1s → 5s → 30s)重試,不會結束。 + 當觸發隱藏/顯示時,交由 `HideController` 編排,流程為「意圖先行」兩段式:`plan_hide` 算出執行計畫(剪掉失效紀錄、補齊 PID)→ 把計畫後的快照寫入 `recovery.json`(先寫入再動手,隱藏中途當機不丟紀錄)→ `commit_hide` 同步隱藏視窗(`SW_HIDE`),並把靜音/凍結/暫停鍵交給副作用專職執行緒(`effects_worker.rs`)按 FIFO 非同步執行——訊息迴圈不被慢操作(音訊列舉、pssuspend 等待)阻塞,快速鍵與介面保持回應。 復原(顯示)時逐條校驗紀錄的有效性:控制代碼須仍存在且仍屬於當初的處理程序(`IsWindow` + PID 比對),凍結/靜音紀錄須符合處理程序建立時刻——控制代碼與 PID 都會被系統回收重複使用,校驗不過的紀錄跳過並如實計入日誌。 diff --git a/docs/zh-tw/dev/ipc-protocol.md b/docs/zh-tw/dev/ipc-protocol.md index d8a652c..33896da 100644 --- a/docs/zh-tw/dev/ipc-protocol.md +++ b/docs/zh-tw/dev/ipc-protocol.md @@ -27,6 +27,8 @@ title: IPC 協定 | `Toggle` | `{"cmd":"toggle"}` | 切換隱藏/顯示 | | `SetAutostart` | `{"cmd":"set_autostart","enabled":true}` | 設定開機自動啟動 | | `SetHotkeys` | `{"cmd":"set_hotkeys","enabled":false}` | 暫時停用/復原快速鍵與滑鼠監控 | +| `ReleaseWindows` | `{"cmd":"release_windows","hwnds":[..]}` | 視窗復原工具:復原顯示指定控制代碼。在核心紀錄裡的視窗按整個處理程序釋放(連同解除凍結/取消靜音);紀錄外的控制代碼直接顯示 | +| `AdoptWindows` | `{"cmd":"adopt_windows","hwnds":[..]}` | 視窗復原工具:隱藏指定控制代碼並納入核心紀錄(享有當機復原保護),不施加靜音/凍結 | | `Quit` | `{"cmd":"quit"}` | 結束核心 | ## Response(核心 → 設定介面) diff --git a/docs/zh-tw/guide/recovery.md b/docs/zh-tw/guide/recovery.md index 3513713..8efbea3 100644 --- a/docs/zh-tw/guide/recovery.md +++ b/docs/zh-tw/guide/recovery.md @@ -18,6 +18,8 @@ title: 視窗復原與當機防護 視窗復原工具也支援將視窗**凍結/解除凍結**,凍結後視窗會被暫停算圖,降低資源消耗。凍結的視窗仍然可透過復原工具找回。 +核心執行時,工具的顯示/隱藏操作會交給核心執行:手動隱藏的視窗同樣納入當機復原保護;復原顯示時會連同解除凍結、取消靜音一起完成。核心未執行時工具直接操作視窗。 + ## 三層防線 Boss Key 核心內建了**當機自癒三層防線**,即使程式意外當機也能盡量保證視窗安全。 From 0a37aa2bb7322c860f6e004fb623ceb0489975c9 Mon Sep 17 00:00:00 2001 From: Ivan Hanloth Date: Sun, 26 Jul 2026 14:54:28 +0100 Subject: [PATCH 15/19] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E6=99=BA=E8=83=BD=E8=BF=BD=E8=B8=AA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/core/Cargo.toml | 1 + crates/core/src/agent.rs | 57 +++++- crates/core/src/hide.rs | 215 ++++++++++++++++++++- crates/core/src/i18n.rs | 6 +- crates/core/src/lib.rs | 1 + crates/core/src/platform/mod.rs | 2 + crates/core/src/platform/win32.rs | 4 + crates/core/src/win_event.rs | 110 +++++++++++ crates/core/tests/agent_window_tracking.rs | 112 +++++++++++ docs/dev/architecture.md | 4 + docs/en/dev/architecture.md | 4 + docs/en/guide/recovery.md | 2 +- docs/guide/recovery.md | 2 +- docs/zh-tw/dev/architecture.md | 3 + docs/zh-tw/guide/recovery.md | 2 +- 15 files changed, 506 insertions(+), 19 deletions(-) create mode 100644 crates/core/src/win_event.rs create mode 100644 crates/core/tests/agent_window_tracking.rs diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 2d9f701..95968c9 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -33,6 +33,7 @@ windows = { workspace = true, features = [ "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_System_Variant", + "Win32_UI_Accessibility", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", diff --git a/crates/core/src/agent.rs b/crates/core/src/agent.rs index daa6b4f..072caaa 100644 --- a/crates/core/src/agent.rs +++ b/crates/core/src/agent.rs @@ -12,9 +12,10 @@ use windows::Win32::UI::Input::KeyboardAndMouse::{ }; use windows::Win32::UI::WindowsAndMessaging::{ AppendMenuW, CW_USEDEFAULT, ChangeWindowMessageFilterEx, CreatePopupMenu, CreateWindowExW, - DefWindowProcW, DestroyMenu, DestroyWindow, DispatchMessageW, GWLP_USERDATA, GetCursorPos, - GetMessageW, GetWindowLongPtrW, KillTimer, MF_CHECKED, MF_SEPARATOR, MF_STRING, MSG, - MSGFLT_ALLOW, PostMessageW, PostQuitMessage, RegisterClassW, SetForegroundWindow, SetTimer, + DefWindowProcW, DestroyMenu, DestroyWindow, DispatchMessageW, EVENT_OBJECT_DESTROY, + EVENT_OBJECT_NAMECHANGE, EVENT_OBJECT_SHOW, GWLP_USERDATA, GetCursorPos, GetMessageW, + GetWindowLongPtrW, KillTimer, MF_CHECKED, MF_SEPARATOR, MF_STRING, MSG, MSGFLT_ALLOW, + PostMessageW, PostQuitMessage, RegisterClassW, SetForegroundWindow, SetTimer, SetWindowLongPtrW, TPM_BOTTOMALIGN, TPM_LEFTALIGN, TrackPopupMenu, TranslateMessage, WINDOW_EX_STYLE, WM_APP, WM_COMMAND, WM_DESTROY, WM_HOTKEY, WM_LBUTTONUP, WM_RBUTTONUP, WM_TIMER, WNDCLASSW, WS_OVERLAPPED, @@ -36,6 +37,7 @@ use crate::platform::win32::WindowsWindowManager; use crate::tray::TrayIcon; use crate::tray_badge::TrayIconSet; use crate::util::append_menu_item; +use crate::win_event::{WM_APP_WINEVENT, WinEventHook}; use crate::{idle, ipc_server, log_error, log_warn, logging, recovery}; const HK_HIDE: i32 = 1; @@ -94,6 +96,8 @@ struct AgentState { effects_worker: Option, /// 恢复文件写入失败是否已提醒过(每次运行只弹一次气泡)。 persist_warned: bool, + /// 窗口事件钩子(销毁 / 显示 / 改标题),实时维护隐藏记录与规则标题。 + win_event_hook: Option, tray: Option, /// 托盘图标的角标变体缓存;未启用托盘时为 None。 tray_icons: Option, @@ -370,6 +374,33 @@ impl AgentState { } } + /// 处理窗口事件:销毁 / 被外部恢复显示的窗口移出隐藏记录, + /// 标题变化同步进隐藏记录与规则(仅内存,随下一次正常落盘写出)。 + fn on_win_event(&mut self, event: u32, hwnd: i64) { + match event { + EVENT_OBJECT_DESTROY | EVENT_OBJECT_SHOW => { + if self.controller.forget_window(hwnd) { + self.persist_recovery(); + self.sync_tray(); + } + } + EVENT_OBJECT_NAMECHANGE => { + let tracked_rule = self + .config + .window_rules + .iter() + .any(|r| r.regex.is_none() && r.hwnd == hwnd); + if !tracked_rule && !self.controller.tracks_window(hwnd) { + return; + } + let title = self.controller.window_title(hwnd); + self.controller.update_title(hwnd, &title); + crate::hide::sync_rule_titles(&mut self.config.window_rules, hwnd, &title); + } + _ => {} + } + } + fn apply_show(&mut self) { let outcome = self.controller.show(); log_show(outcome); @@ -538,12 +569,12 @@ fn log_hide(config: &Config, outcomes: &[RuleOutcome], plan: &HidePlan) { } } -/// 记录恢复结果;有失效记录被跳过时如实写出。 +/// 记录恢复结果;失效与找回的数量如实写出。 fn log_show(outcome: ShowOutcome) { if outcome.stale > 0 { logging::info(&format!( - "恢复显示 {} 个窗口,另有 {} 条记录已失效(窗口已关闭或句柄被复用),已跳过", - outcome.shown, outcome.stale + "恢复显示 {} 个窗口;{} 条记录句柄已失效,其中 {} 个按进程路径与标题重新找回", + outcome.shown, outcome.stale, outcome.refound )); } else { logging::info(&format!("恢复显示 {} 个窗口", outcome.shown)); @@ -833,6 +864,10 @@ unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: } LRESULT(0) } + WM_APP_WINEVENT => { + state.on_win_event(wparam.0 as u32, lparam.0 as i64); + LRESULT(0) + } WM_APP_IPC => { let mut should_quit = false; while let Ok((cmd, reply_tx)) = state.ipc_rx.try_recv() { @@ -1111,6 +1146,7 @@ pub fn run(options: AgentOptions) { controller: HideController::new(WindowsWindowManager, effects_worker.effects()), effects_worker: Some(effects_worker), persist_warned: false, + win_event_hook: None, tray, tray_icons, ipc_rx, @@ -1156,7 +1192,14 @@ pub fn run(options: AgentOptions) { ); } - state.borrow_mut().sync_monitoring(hwnd); + { + let mut state = state.borrow_mut(); + state.win_event_hook = WinEventHook::install(hwnd); + if state.win_event_hook.is_none() { + log_warn!("窗口事件钩子安装失败,隐藏记录仅在触发隐藏 / 恢复时维护"); + } + state.sync_monitoring(hwnd); + } let hwnd_value = hwnd.0 as isize; ipc_server::spawn(options.pipe_name.clone(), move |cmd| { diff --git a/crates/core/src/hide.rs b/crates/core/src/hide.rs index acf6e4f..7c47b5b 100644 --- a/crates/core/src/hide.rs +++ b/crates/core/src/hide.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use bosskey_common::matching::{WindowResolution, match_process_rule, resolve_window_rule}; -use bosskey_common::{Config, Setting, WindowInfo}; +use bosskey_common::{Config, NO_TITLE, Setting, WindowInfo, WindowRule}; use serde::{Deserialize, Serialize}; use crate::effects::Effects; @@ -210,6 +210,25 @@ pub struct ShowOutcome { pub shown: usize, /// 因句柄失效或已被其他窗口复用而跳过的记录数。 pub stale: usize, + /// 句柄失效后按「进程路径 + 标题」重新找回并显示的窗口数。 + pub refound: usize, +} + +/// 把标题变化同步进句柄匹配的精确窗口规则(仅内存,随下次配置保存落盘)。 +/// `NO_TITLE` 不参与同步。返回是否有规则被更新。 +pub fn sync_rule_titles(rules: &mut [WindowRule], hwnd: i64, title: &str) -> bool { + if title == NO_TITLE { + return false; + } + let mut changed = false; + for rule in rules + .iter_mut() + .filter(|r| r.regex.is_none() && r.hwnd == hwnd && r.title != title) + { + rule.title = title.to_string(); + changed = true; + } + changed } pub struct HideController { @@ -251,6 +270,42 @@ impl HideController { self.wm.foreground() } + /// 窗口当前标题(封装 wm 依赖,供事件追踪查询)。 + pub fn window_title(&self, hwnd: i64) -> String { + self.wm.window_title(hwnd) + } + + /// 该句柄是否在隐藏记录里。 + pub fn tracks_window(&self, hwnd: i64) -> bool { + self.hidden.iter().any(|t| t.hwnd == hwnd) + } + + /// 移除句柄对应的隐藏记录(窗口已销毁或被外部恢复显示时由事件追踪调用)。 + /// 返回是否有记录被移除。 + pub fn forget_window(&mut self, hwnd: i64) -> bool { + let before = self.hidden.len(); + self.hidden.retain(|t| t.hwnd != hwnd); + self.hidden.len() != before + } + + /// 同步隐藏记录里的窗口标题(标题变化事件);`NO_TITLE` 不参与同步。 + /// 返回是否有记录被更新。 + pub fn update_title(&mut self, hwnd: i64, title: &str) -> bool { + if title == NO_TITLE { + return false; + } + let mut changed = false; + for t in self + .hidden + .iter_mut() + .filter(|t| t.hwnd == hwnd && t.title != title) + { + t.title = title.to_string(); + changed = true; + } + changed + } + /// 计算一次隐藏的执行计划,不做任何窗口 / 副作用动作;顺带完成隐藏集剪枝 /// 与 PID 补查(仍查不到的目标剔除)。`freeze_pids` 为最终要冻结的 PID 集 /// (可能已含子进程树),仅在 `freeze_after_hide` 开启时生效。 @@ -393,6 +448,7 @@ impl HideController { } let hidden = std::mem::take(&mut self.hidden); + let mut stale: Vec<&Target> = Vec::new(); for t in &hidden { // 句柄仍存活且仍属于当初的进程才恢复,避免弹出复用同一句柄的无关窗口。 if self.wm.is_window(t.hwnd) && (t.pid == 0 || self.wm.window_pid(t.hwnd) == t.pid) { @@ -400,8 +456,10 @@ impl HideController { outcome.shown += 1; } else { outcome.stale += 1; + stale.push(t); } } + outcome.refound = self.refind_stale(&hidden, &stale); let muted = std::mem::take(&mut self.muted); for r in &muted { @@ -412,6 +470,36 @@ impl HideController { outcome } + /// 按「进程路径 + 标题」为失效记录找回窗口:只匹配当前不可见、且不在 + /// 本次隐藏集内的窗口,找到即恢复显示。返回找回数。 + fn refind_stale(&self, hidden: &[Target], stale: &[&Target]) -> usize { + if stale + .iter() + .all(|t| t.process_path.is_empty() || t.title.is_empty() || t.title == NO_TITLE) + { + return 0; + } + let windows = self.wm.enumerate(); + let mut used: HashSet = hidden.iter().map(|t| t.hwnd).collect(); + let mut refound = 0; + for t in stale { + if t.process_path.is_empty() || t.title.is_empty() || t.title == NO_TITLE { + continue; + } + if let Some(w) = windows.iter().find(|w| { + !w.visible + && !used.contains(&w.hwnd) + && w.path == t.process_path + && w.title == t.title + }) { + used.insert(w.hwnd); + self.wm.show(w.hwnd); + refound += 1; + } + } + refound + } + /// 释放指定进程的隐藏状态:显示窗口、解冻、取消静音,并从隐藏集移除。返回被释放的窗口数。 pub fn release_pids(&mut self, pids: &[u32]) -> usize { if pids.is_empty() { @@ -797,12 +885,14 @@ mod tests { } impl WindowManager for MockWm { + // 与真实平台一致:不可见窗口也在枚举结果里,visible 标记如实反映当前状态。 fn enumerate(&self) -> Vec { let visible = self.visible.borrow(); + let exists = self.exists.borrow(); self.windows .iter() - .filter(|w| visible.contains(&w.hwnd)) - .cloned() + .filter(|w| exists.contains(&w.hwnd)) + .map(|w| w.clone().with_visibility(visible.contains(&w.hwnd))) .collect() } fn hide(&self, hwnd: i64) { @@ -833,6 +923,13 @@ mod tests { .map(|w| w.pid) .unwrap_or(0) } + fn window_title(&self, hwnd: i64) -> String { + self.windows + .iter() + .find(|w| w.hwnd == hwnd) + .map(|w| w.title.clone()) + .unwrap_or_else(|| bosskey_common::NO_TITLE.to_string()) + } fn process_start_time(&self, pid: u32) -> i64 { if pid == 0 { return 0; @@ -895,7 +992,7 @@ mod tests { assert_eq!(*controller.effects.pauses.borrow(), 1, "应发送一次暂停键"); let outcome = controller.show(); - assert_eq!(outcome, ShowOutcome { shown: 1, stale: 0 }); + assert_eq!(outcome, ShowOutcome { shown: 1, stale: 0, refound: 0 }); assert!(!controller.is_hidden()); assert!(controller.wm.is_visible(10), "恢复后微信应可见"); assert_eq!(*controller.effects.resumes.borrow(), vec![10], "应解冻"); @@ -1115,7 +1212,7 @@ mod tests { ..Default::default() }); - assert_eq!(outcome, ShowOutcome { shown: 1, stale: 0 }); + assert_eq!(outcome, ShowOutcome { shown: 1, stale: 0, refound: 0 }); assert!(!controller.is_hidden(), "恢复完成后应回到未隐藏状态"); assert!(controller.wm.is_visible(10), "崩溃前隐藏的窗口应被找回"); assert_eq!(*controller.effects.resumes.borrow(), vec![10], "应解冻进程"); @@ -1183,7 +1280,7 @@ mod tests { let outcome = controller.show(); assert_eq!( outcome, - ShowOutcome { shown: 1, stale: 2 }, + ShowOutcome { shown: 1, stale: 2, refound: 0 }, "死句柄与被复用句柄都应跳过" ); assert!(!controller.wm.is_visible(20), "被复用的句柄不得被弹出来"); @@ -1249,6 +1346,112 @@ mod tests { ); } + #[test] + fn forget_window_removes_record_and_update_title_syncs_it() { + let setting = Setting { + hide_current: false, + ..Setting::default() + }; + let wm = MockWm::new(vec![win("微信", 10, "WeChat.exe", "C:\\WeChat.exe")], 0); + let mut controller = HideController::new(wm, MockEffects::default()); + controller.apply_hide(&setting, &[Target::from_window(&win("微信", 10, "WeChat.exe", "C:\\WeChat.exe"))], &[]); + + assert!(controller.tracks_window(10)); + assert!(controller.update_title(10, "微信 - 新会话")); + assert!(!controller.update_title(10, "微信 - 新会话"), "标题未变不算更新"); + assert!( + !controller.update_title(10, bosskey_common::NO_TITLE), + "NO_TITLE 不参与同步" + ); + assert_eq!(controller.snapshot().hidden[0].title, "微信 - 新会话"); + + assert!(!controller.forget_window(99), "未记录的句柄无事发生"); + assert!(controller.forget_window(10)); + assert!(!controller.is_hidden(), "记录移除后不再处于隐藏态"); + } + + #[test] + fn sync_rule_titles_updates_exact_rules_only() { + let mut rules = vec![ + wrule("旧标题", 10, "app.exe", "C:\\app.exe"), + WindowRule::from_regex("^项目"), + ]; + assert!(sync_rule_titles(&mut rules, 10, "新标题")); + assert_eq!(rules[0].title, "新标题"); + assert!(!sync_rule_titles(&mut rules, 10, "新标题"), "标题未变不算更新"); + assert!(!sync_rule_titles(&mut rules, 99, "别的"), "句柄不匹配不更新"); + assert!( + !sync_rule_titles(&mut rules, 10, NO_TITLE), + "NO_TITLE 不参与同步" + ); + } + + #[test] + fn show_refinds_stale_records_by_path_and_title() { + let setting = Setting { + hide_current: false, + ..Setting::default() + }; + // 99 是同进程同标题的新窗口(程序重建了窗口),当前不可见。 + let wm = MockWm::new( + vec![ + win_pid("微信", 10, "WeChat.exe", 500, "C:\\WeChat.exe"), + win_pid("微信", 99, "WeChat.exe", 600, "C:\\WeChat.exe"), + ], + 0, + ); + wm.hide(99); + let mut controller = HideController::new(wm, MockEffects::default()); + controller.apply_hide( + &setting, + &[Target::from_window(&win_pid( + "微信", + 10, + "WeChat.exe", + 500, + "C:\\WeChat.exe", + ))], + &[], + ); + + controller.wm.destroy(10); + let outcome = controller.show(); + assert_eq!( + outcome, + ShowOutcome { + shown: 0, + stale: 1, + refound: 1 + } + ); + assert!( + controller.wm.is_visible(99), + "应按进程路径 + 标题找回重建的窗口" + ); + } + + #[test] + fn refind_skips_visible_windows_and_records_without_info() { + let setting = Setting { + hide_current: false, + ..Setting::default() + }; + let wm = MockWm::new( + vec![ + win_pid("微信", 10, "WeChat.exe", 500, "C:\\WeChat.exe"), + win_pid("微信", 99, "WeChat.exe", 600, "C:\\WeChat.exe"), + ], + 0, + ); + let mut controller = HideController::new(wm, MockEffects::default()); + // bare 目标没有路径 / 标题信息,失效后无从找回。 + controller.apply_hide(&setting, &[Target::bare(10, 500)], &[]); + controller.wm.destroy(10); + let outcome = controller.show(); + assert_eq!(outcome.refound, 0, "无路径 / 标题信息的记录不找回"); + assert!(controller.wm.is_visible(99), "可见窗口不受影响"); + } + #[test] fn release_windows_frees_whole_process_and_shows_unknown_handles() { let setting = Setting { diff --git a/crates/core/src/i18n.rs b/crates/core/src/i18n.rs index fd03cbf..235c500 100644 --- a/crates/core/src/i18n.rs +++ b/crates/core/src/i18n.rs @@ -85,7 +85,7 @@ impl Msg { Msg::ErrCoreExited => "核心已退出", Msg::ErrNotifyCore => "无法通知核心", Msg::ErrCoreTimeout => "核心响应超时", - Msg::ErrCoreExeMissing => "未找到核心程序 {exe}", + Msg::ErrCoreExeMissing => "未找到核心程序 {exe}。它很可能被杀毒软件拦截或隔离了:请尝试将 Boss Key 的程序目录加入杀毒软件的白名单 / 信任区,再从隔离区恢复该文件;若无法恢复,请重新下载完整程序包。", Msg::ErrFreezePartial => "{failed}/{total} 个进程冻结失败", Msg::ErrResumePartial => "{failed}/{total} 个进程解冻失败", Msg::ErrUrlSchemeNotAllowed => "只允许打开 http/https/mailto 链接", @@ -123,7 +123,7 @@ impl Msg { Msg::ErrCoreExited => "The core has exited", Msg::ErrNotifyCore => "Cannot reach the core", Msg::ErrCoreTimeout => "The core did not respond in time", - Msg::ErrCoreExeMissing => "Core program {exe} not found", + Msg::ErrCoreExeMissing => "Core program {exe} not found. It was most likely blocked or quarantined by antivirus software: add the Boss Key program folder to your antivirus allowlist, then restore the file from quarantine; if that is not possible, download the full package again.", Msg::ErrFreezePartial => "Failed to freeze {failed} of {total} processes", Msg::ErrResumePartial => "Failed to resume {failed} of {total} processes", Msg::ErrUrlSchemeNotAllowed => "Only http/https/mailto links may be opened", @@ -161,7 +161,7 @@ impl Msg { Msg::ErrCoreExited => "核心已結束", Msg::ErrNotifyCore => "無法通知核心", Msg::ErrCoreTimeout => "核心回應逾時", - Msg::ErrCoreExeMissing => "找不到核心程式 {exe}", + Msg::ErrCoreExeMissing => "找不到核心程式 {exe}。它很可能被防毒軟體攔截或隔離了:請將 Boss Key 的程式資料夾加入防毒軟體的信任區/白名單,再從隔離區還原該檔案;若無法還原,請重新下載完整程式包。", Msg::ErrFreezePartial => "{failed}/{total} 個程序凍結失敗", Msg::ErrResumePartial => "{failed}/{total} 個程序解除凍結失敗", Msg::ErrUrlSchemeNotAllowed => "僅允許開啟 http/https/mailto 連結", diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 7570b5e..6d4c608 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -22,5 +22,6 @@ pub mod shell; pub mod single_instance; pub mod tray; pub mod tray_badge; +pub mod win_event; mod util; diff --git a/crates/core/src/platform/mod.rs b/crates/core/src/platform/mod.rs index 42b6a9f..febd813 100644 --- a/crates/core/src/platform/mod.rs +++ b/crates/core/src/platform/mod.rs @@ -12,6 +12,8 @@ pub trait WindowManager { fn is_window(&self, hwnd: i64) -> bool; /// 句柄当前所属进程的 PID;查不到返回 0。 fn window_pid(&self, hwnd: i64) -> u32; + /// 窗口当前标题;无标题或查不到时返回 `NO_TITLE` 占位。 + fn window_title(&self, hwnd: i64) -> String; /// 进程创建时刻(Unix 毫秒),用于识别 PID 复用;查不到返回 0。 fn process_start_time(&self, pid: u32) -> i64; } diff --git a/crates/core/src/platform/win32.rs b/crates/core/src/platform/win32.rs index 86e2e86..0b2c00a 100644 --- a/crates/core/src/platform/win32.rs +++ b/crates/core/src/platform/win32.rs @@ -190,6 +190,10 @@ impl WindowManager for WindowsWindowManager { window_pid(hwnd_from(hwnd)) } + fn window_title(&self, hwnd: i64) -> String { + window_title(hwnd_from(hwnd)) + } + fn process_start_time(&self, pid: u32) -> i64 { process_start_time(pid) } diff --git a/crates/core/src/win_event.rs b/crates/core/src/win_event.rs new file mode 100644 index 0000000..7731e68 --- /dev/null +++ b/crates/core/src/win_event.rs @@ -0,0 +1,110 @@ +//! 窗口事件追踪:用 `SetWinEventHook` 订阅顶层窗口的销毁 / 显示 / 改标题事件, +//! 转发给代理窗口,由 agent 实时维护隐藏记录与规则信息。 +//! 回调只做过滤与 `PostMessageW`,重活都在代理窗口的消息处理里。 + +use std::sync::atomic::{AtomicIsize, Ordering::Relaxed}; + +use windows::Win32::Foundation::{HWND, LPARAM, WPARAM}; +use windows::Win32::UI::Accessibility::{HWINEVENTHOOK, SetWinEventHook, UnhookWinEvent}; +use windows::Win32::UI::WindowsAndMessaging::{ + CHILDID_SELF, EVENT_OBJECT_DESTROY, EVENT_OBJECT_NAMECHANGE, EVENT_OBJECT_SHOW, OBJID_WINDOW, + PostMessageW, WINEVENT_OUTOFCONTEXT, WM_APP, +}; + +/// 窗口事件转发给代理窗口的消息;`wparam` 为事件号,`lparam` 为窗口句柄。 +pub const WM_APP_WINEVENT: u32 = WM_APP + 6; + +static HWND_RAW: AtomicIsize = AtomicIsize::new(0); + +/// 事件是否需要转发:只关心顶层窗口自身的销毁 / 显示 / 改标题。 +fn relevant(event: u32, id_object: i32, id_child: i32) -> bool { + id_object == OBJID_WINDOW.0 + && id_child == CHILDID_SELF as i32 + && matches!( + event, + EVENT_OBJECT_DESTROY | EVENT_OBJECT_SHOW | EVENT_OBJECT_NAMECHANGE + ) +} + +unsafe extern "system" fn hook_proc( + _hook: HWINEVENTHOOK, + event: u32, + hwnd: HWND, + id_object: i32, + id_child: i32, + _id_thread: u32, + _time: u32, +) { + if !relevant(event, id_object, id_child) || hwnd.is_invalid() { + return; + } + let raw = HWND_RAW.load(Relaxed); + if raw == 0 { + return; + } + unsafe { + let agent = HWND(raw as *mut std::ffi::c_void); + let _ = PostMessageW( + Some(agent), + WM_APP_WINEVENT, + WPARAM(event as usize), + LPARAM(hwnd.0 as isize), + ); + } +} + +/// 窗口事件钩子;析构时卸载。须在有消息循环的线程(代理窗口线程)上安装。 +pub struct WinEventHook { + handle: HWINEVENTHOOK, +} + +impl WinEventHook { + /// 一次挂钩覆盖 DESTROY(0x8001)–NAMECHANGE(0x800C) 区间,回调里再过滤。 + pub fn install(agent_hwnd: HWND) -> Option { + HWND_RAW.store(agent_hwnd.0 as isize, Relaxed); + let handle = unsafe { + SetWinEventHook( + EVENT_OBJECT_DESTROY, + EVENT_OBJECT_NAMECHANGE, + None, + Some(hook_proc), + 0, + 0, + WINEVENT_OUTOFCONTEXT, + ) + }; + if handle.is_invalid() { + None + } else { + Some(WinEventHook { handle }) + } + } +} + +impl Drop for WinEventHook { + fn drop(&mut self) { + unsafe { + let _ = UnhookWinEvent(self.handle); + } + HWND_RAW.store(0, Relaxed); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use windows::Win32::UI::WindowsAndMessaging::{EVENT_OBJECT_FOCUS, EVENT_OBJECT_HIDE, OBJID_CURSOR}; + + #[test] + fn only_toplevel_destroy_show_namechange_are_relevant() { + let obj = OBJID_WINDOW.0; + let child = CHILDID_SELF as i32; + assert!(relevant(EVENT_OBJECT_DESTROY, obj, child)); + assert!(relevant(EVENT_OBJECT_SHOW, obj, child)); + assert!(relevant(EVENT_OBJECT_NAMECHANGE, obj, child)); + assert!(!relevant(EVENT_OBJECT_HIDE, obj, child), "隐藏事件不用追踪"); + assert!(!relevant(EVENT_OBJECT_FOCUS, obj, child), "区间内的无关事件应过滤"); + assert!(!relevant(EVENT_OBJECT_DESTROY, OBJID_CURSOR.0, child), "非窗口对象应过滤"); + assert!(!relevant(EVENT_OBJECT_DESTROY, obj, 3), "子对象事件应过滤"); + } +} diff --git a/crates/core/tests/agent_window_tracking.rs b/crates/core/tests/agent_window_tracking.rs new file mode 100644 index 0000000..d662503 --- /dev/null +++ b/crates/core/tests/agent_window_tracking.rs @@ -0,0 +1,112 @@ +//! 端到端:被隐藏的窗口自行销毁后,窗口事件追踪应实时移除记录并更新恢复文件。 + +use std::time::{Duration, Instant}; + +use bosskey_common::ipc::{Command, PipeClient, Response}; +use bosskey_core::agent::{self, AgentOptions}; +use bosskey_core::recovery; +use windows::Win32::Foundation::{LPARAM, WPARAM}; +use windows::Win32::System::Threading::GetCurrentThreadId; +use windows::Win32::UI::WindowsAndMessaging::{ + CW_USEDEFAULT, CreateWindowExW, DestroyWindow, DispatchMessageW, GetMessageW, MSG, + PostThreadMessageW, SW_SHOW, ShowWindow, TranslateMessage, WINDOW_EX_STYLE, WM_QUIT, + WS_OVERLAPPEDWINDOW, +}; +use windows::core::w; + +/// 在带消息循环的独立线程上创建一个可见窗口;线程收到 WM_QUIT 后销毁窗口。 +fn spawn_visible_window() -> (i64, u32, std::thread::JoinHandle<()>) { + let (tx, rx) = std::sync::mpsc::channel::<(i64, u32)>(); + let handle = std::thread::spawn(move || unsafe { + let hwnd = CreateWindowExW( + WINDOW_EX_STYLE(0), + w!("Static"), + w!("BossKeyTrackingTestWindow"), + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + 200, + 120, + None, + None, + None, + None, + ) + .expect("创建测试窗口失败"); + let _ = ShowWindow(hwnd, SW_SHOW); + tx.send((hwnd.0 as isize as i64, GetCurrentThreadId())) + .unwrap(); + + let mut msg: MSG = std::mem::zeroed(); + while GetMessageW(&mut msg, None, 0, 0).0 > 0 { + let _ = TranslateMessage(&msg); + DispatchMessageW(&msg); + } + let _ = DestroyWindow(hwnd); + }); + let (hwnd, tid) = rx.recv().expect("窗口线程未上报句柄"); + (hwnd, tid, handle) +} + +#[test] +fn destroying_a_hidden_window_clears_the_record_in_real_time() { + let (hwnd, window_tid, window_thread) = spawn_visible_window(); + + let dir = tempfile::tempdir().unwrap(); + let config_path = dir.path().join("config.json"); + bosskey_common::Config::default() + .save(&config_path) + .unwrap(); + let recovery_path = dir.path().join(recovery::RECOVERY_FILE_NAME); + + let pipe = r"\\.\pipe\bosskey_test_window_tracking"; + let options = AgentOptions { + config_path, + pipe_name: pipe.to_string(), + enable_tray: false, + auto_quit_ms: Some(15_000), + }; + let agent_thread = std::thread::spawn(move || agent::run(options)); + let client = PipeClient::new(pipe); + + let reply = client + .send(&Command::AdoptWindows { hwnds: vec![hwnd] }) + .unwrap(); + assert_eq!(reply, Response::Ok); + assert_eq!( + client.send(&Command::GetState).unwrap(), + Response::State { hidden: true } + ); + assert!(recovery_path.exists()); + + // 结束窗口线程 → 窗口销毁 → 事件钩子应让核心实时移除记录。 + unsafe { + let _ = PostThreadMessageW(window_tid, WM_QUIT, WPARAM(0), LPARAM(0)); + } + window_thread.join().unwrap(); + + let deadline = Instant::now() + Duration::from_secs(5); + loop { + if client.send(&Command::GetState).unwrap() == (Response::State { hidden: false }) { + break; + } + assert!( + Instant::now() < deadline, + "窗口销毁后核心未在时限内移除隐藏记录" + ); + std::thread::sleep(Duration::from_millis(50)); + } + assert!( + !recovery_path.exists(), + "记录清空后恢复文件应同步清除" + ); + + let quit = client.send(&Command::Quit).unwrap(); + assert_eq!(quit, Response::Ok); + let deadline = Instant::now() + Duration::from_secs(10); + while !agent_thread.is_finished() { + assert!(Instant::now() < deadline, "代理线程未在退出命令后结束"); + std::thread::sleep(Duration::from_millis(50)); + } + agent_thread.join().unwrap(); +} diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index 0793809..a4e44c1 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -88,6 +88,7 @@ Boss-Key/ │ mouse_hook.rs WH_MOUSE_LL(中键/侧键/四角) │ keyboard_hook.rs WH_KEYBOARD_LL(「不传递」热键拦截) │ idle.rs GetLastInputInfo 空闲 + 自动隐藏判定 +│ win_event.rs SetWinEventHook 窗口事件追踪(销毁/显示/改标题 → 实时维护记录) │ tray.rs Shell_NotifyIcon 托盘 + 气泡 │ ipc_server.rs 命名管道服务端(创建失败退避重试,不退出) │ autostart.rs 开机自启(计划任务 XML 含失败自动重启 + 注册表回退) @@ -120,10 +121,13 @@ Boss-Key/ - 鼠标钩子(中键 / 侧键 / 四角); - 命名管道服务端(来自配置界面的命令); - 定时器(空闲检测、状态维护等); +- 窗口事件(`SetWinEventHook`:顶层窗口销毁 / 显示 / 改标题); - 托盘图标交互。 消息循环状态由 `RefCell` 承载:托盘 / 悬浮窗菜单的模态循环(`TrackPopupMenu`)会重入 `wndproc`,重入期间的事件借用失败即被安全丢弃,避免出现两个可变引用的别名。IPC 线程创建命名管道失败时按退避(1s → 5s → 30s)重试,不会退出。 +窗口事件驱动隐藏记录的实时维护:被隐藏的窗口自行销毁或被外部恢复显示时,记录即刻移除并落盘;标题变化同步进隐藏记录与精确窗口规则(仅内存,随下次正常落盘写出),使「标题 + 进程路径」的追溯与找回始终基于最新信息。恢复时若句柄已失效,还会按「进程路径 + 标题」在当前不可见窗口中尝试重新找回。 + 当触发隐藏 / 显示时,交由 `HideController` 编排,流程为「意图先行」两段式:`plan_hide` 算出执行计划(剪掉失效记录、补齐 PID)→ 把计划后的快照写入 `recovery.json`(先落盘再动手,隐藏中途崩溃不丢记录)→ `commit_hide` 同步隐藏窗口(`SW_HIDE`),并把静音 / 冻结 / 暂停键交给副作用专职线程(`effects_worker.rs`)按 FIFO 异步执行——消息循环不被慢操作(音频枚举、pssuspend 等待)阻塞,热键与界面保持响应。 恢复(显示)时逐条校验记录的有效性:句柄须仍存在且仍属于当初的进程(`IsWindow` + PID 比对),冻结 / 静音记录须匹配进程创建时刻——句柄与 PID 都会被系统回收复用,校验不过的记录跳过并如实计入日志。 diff --git a/docs/en/dev/architecture.md b/docs/en/dev/architecture.md index ead110c..3ee898e 100644 --- a/docs/en/dev/architecture.md +++ b/docs/en/dev/architecture.md @@ -88,6 +88,7 @@ Boss-Key/ │ mouse_hook.rs WH_MOUSE_LL (middle/side buttons, corners) │ keyboard_hook.rs WH_KEYBOARD_LL ("don't pass through" hotkey interception) │ idle.rs GetLastInputInfo idle detection + auto-hide decision +│ win_event.rs SetWinEventHook window-event tracking (destroy/show/title change → live record upkeep) │ tray.rs Shell_NotifyIcon tray + balloons │ ipc_server.rs Named-pipe server (retries pipe creation with backoff instead of exiting) │ autostart.rs Startup (scheduled-task XML with restart-on-failure + registry fallback) @@ -120,10 +121,13 @@ Boss-Key/ - The mouse hook (middle / side buttons, corners); - The named-pipe server (commands from the settings window); - Timers (idle detection, state maintenance, and so on); +- Window events (`SetWinEventHook`: top-level window destroy / show / title change); - Tray icon interaction. Message-loop state lives in a `RefCell`: the modal loops of the tray / floating-window menus (`TrackPopupMenu`) re-enter `wndproc`, and events arriving during re-entry fail the borrow and are safely dropped, so no aliased mutable references can exist. The IPC thread retries pipe creation with backoff (1s → 5s → 30s) instead of exiting. +Window events keep the hidden records maintained in real time: when a hidden window is destroyed or shown externally, its record is removed and persisted immediately; title changes are synced into hidden records and exact window rules (in memory only, written out with the next regular persist), so reacquisition and refinding by "title + process path" always work on fresh data. On restore, records with dead handles are additionally refound among currently invisible windows by "process path + title". + When hiding or showing is triggered, `HideController` orchestrates it with a two-phase, intent-first flow: `plan_hide` computes the execution plan (pruning stale records and backfilling PIDs) → the planned snapshot is written to `recovery.json` (persist first, act second — a crash mid-hide loses no records) → `commit_hide` hides the windows synchronously (`SW_HIDE`) and hands muting / freezing / the pause key to the dedicated side-effect thread (`effects_worker.rs`), executed asynchronously in FIFO order — the message loop is never blocked by slow operations (audio enumeration, waiting on pssuspend), so hotkeys and the UI stay responsive. When restoring (showing), every record is validated first: the handle must still exist and still belong to the original process (`IsWindow` + PID comparison), and frozen / muted records must match the process creation time — both handles and PIDs are recycled by the system, and records that fail validation are skipped and reported truthfully in the log. diff --git a/docs/en/guide/recovery.md b/docs/en/guide/recovery.md index 4e177e3..2cf1643 100644 --- a/docs/en/guide/recovery.md +++ b/docs/en/guide/recovery.md @@ -32,7 +32,7 @@ The core writes key events and panic information to log files in the `logs` fold Each time windows are hidden, the core records which windows were hidden and which processes were frozen or muted into `recovery.json`, and deletes the file on a normal restore or exit. -If that file still exists when the core restarts, the previous run **exited abnormally** — so the core automatically **restores the windows, resumes the processes and unmutes them**. +If that file still exists when the core restarts, the previous run **exited abnormally** — so the core automatically **restores the windows, resumes the processes and unmutes them**. If a window handle is no longer valid (for example the app recreated its window), the core additionally tries to refind the window by process path and title. ### Layer 3: the scheduled task diff --git a/docs/guide/recovery.md b/docs/guide/recovery.md index 864634b..7cfe654 100644 --- a/docs/guide/recovery.md +++ b/docs/guide/recovery.md @@ -32,7 +32,7 @@ Boss Key 核心内置了**崩溃自愈三层防线**,即使程序意外崩溃 每次隐藏窗口时,核心会把"隐藏了哪些窗口、冻结 / 静音了哪些进程"写入 `recovery.json`;正常显示或退出时删除该文件。 -核心重启时若发现该文件仍然存在,说明上次是**异常退出**——于是它会自动**找回窗口、解冻进程、取消静音**。 +核心重启时若发现该文件仍然存在,说明上次是**异常退出**——于是它会自动**找回窗口、解冻进程、取消静音**。恢复时若窗口句柄已失效(如程序重建了窗口),核心还会按进程路径与标题尝试重新找回。 ### 第三层:计划任务 diff --git a/docs/zh-tw/dev/architecture.md b/docs/zh-tw/dev/architecture.md index c119da5..dffd501 100644 --- a/docs/zh-tw/dev/architecture.md +++ b/docs/zh-tw/dev/architecture.md @@ -120,10 +120,13 @@ Boss-Key/ - 滑鼠掛鉤(中鍵/側鍵/四角); - 具名管道伺服端(來自設定介面的命令); - 計時器(閒置偵測、狀態維護等); +- 視窗事件(`SetWinEventHook`:頂層視窗銷毀/顯示/改標題); - 通知區域圖示互動。 訊息迴圈狀態由 `RefCell` 承載:通知區域/懸浮窗選單的強制回應迴圈(`TrackPopupMenu`)會重入 `wndproc`,重入期間的事件借用失敗即被安全丟棄,避免出現兩個可變參考的別名。IPC 執行緒建立具名管道失敗時按退避(1s → 5s → 30s)重試,不會結束。 +視窗事件驅動隱藏紀錄的即時維護:被隱藏的視窗自行銷毀或被外部復原顯示時,紀錄即刻移除並寫入磁碟;標題變化同步進隱藏紀錄與精確視窗規則(僅記憶體,隨下次正常寫入時落盤),使「標題 + 程序路徑」的追溯與找回始終基於最新資訊。復原時若控制代碼已失效,還會按「程序路徑 + 標題」在目前不可見視窗中嘗試重新找回。 + 當觸發隱藏/顯示時,交由 `HideController` 編排,流程為「意圖先行」兩段式:`plan_hide` 算出執行計畫(剪掉失效紀錄、補齊 PID)→ 把計畫後的快照寫入 `recovery.json`(先寫入再動手,隱藏中途當機不丟紀錄)→ `commit_hide` 同步隱藏視窗(`SW_HIDE`),並把靜音/凍結/暫停鍵交給副作用專職執行緒(`effects_worker.rs`)按 FIFO 非同步執行——訊息迴圈不被慢操作(音訊列舉、pssuspend 等待)阻塞,快速鍵與介面保持回應。 復原(顯示)時逐條校驗紀錄的有效性:控制代碼須仍存在且仍屬於當初的處理程序(`IsWindow` + PID 比對),凍結/靜音紀錄須符合處理程序建立時刻——控制代碼與 PID 都會被系統回收重複使用,校驗不過的紀錄跳過並如實計入日誌。 diff --git a/docs/zh-tw/guide/recovery.md b/docs/zh-tw/guide/recovery.md index 8efbea3..983d322 100644 --- a/docs/zh-tw/guide/recovery.md +++ b/docs/zh-tw/guide/recovery.md @@ -32,7 +32,7 @@ Boss Key 核心內建了**當機自癒三層防線**,即使程式意外當機 每次隱藏視窗時,核心會把「隱藏了哪些視窗、凍結/靜音了哪些程序」寫入 `recovery.json`;正常顯示或結束時刪除該檔案。 -核心重新啟動時若發現該檔案仍然存在,表示上次是**異常結束**——於是它會自動**找回視窗、解除凍結程序、取消靜音**。 +核心重新啟動時若發現該檔案仍然存在,表示上次是**異常結束**——於是它會自動**找回視窗、解除凍結程序、取消靜音**。復原時若視窗控制代碼已失效(如程式重建了視窗),核心還會按程序路徑與標題嘗試重新找回。 ### 第三層:排程工作 From d33282a000d4610faae1e5a0121c144918f2a5d5 Mon Sep 17 00:00:00 2001 From: Ivan Hanloth Date: Sun, 26 Jul 2026 15:06:54 +0100 Subject: [PATCH 16/19] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=B7=B2?= =?UTF-8?q?=E9=9A=90=E8=97=8F=E7=9A=84=E7=AA=97=E5=8F=A3=E4=BC=9A=E8=A2=AB?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/core/src/agent.rs | 11 ++- crates/core/src/effects_worker.rs | 5 +- crates/core/src/hide.rs | 108 +++++++++++++++++++-- crates/core/src/i18n.rs | 16 ++- crates/core/src/logging.rs | 5 +- crates/core/src/recovery.rs | 5 +- crates/core/src/win_event.rs | 14 ++- crates/core/tests/agent_window_tracking.rs | 5 +- docs/en/guide/faq.md | 4 + docs/guide/faq.md | 4 + docs/zh-tw/guide/faq.md | 4 + 11 files changed, 151 insertions(+), 30 deletions(-) diff --git a/crates/core/src/agent.rs b/crates/core/src/agent.rs index 072caaa..f847229 100644 --- a/crates/core/src/agent.rs +++ b/crates/core/src/agent.rs @@ -516,8 +516,10 @@ impl AgentState { (Response::Ok, false) } Command::AdoptWindows { hwnds } => { - let targets: Vec = - hwnds.iter().map(|&h| crate::hide::Target::bare(h, 0)).collect(); + let targets: Vec = hwnds + .iter() + .map(|&h| crate::hide::Target::bare(h, 0)) + .collect(); // 恢复工具的手动隐藏不施加副作用,仅隐藏并纳入记录。 let mut setting = self.config.setting.clone(); setting.mute_after_hide = false; @@ -1090,7 +1092,10 @@ pub fn run(options: AgentOptions) { let hwnd = match create_agent_window() { Ok(hwnd) => hwnd, Err(e) => { - log_error!("创建代理窗口失败,核心无法启动: {}", crate::util::win_err(&e)); + log_error!( + "创建代理窗口失败,核心无法启动: {}", + crate::util::win_err(&e) + ); return; } }; diff --git a/crates/core/src/effects_worker.rs b/crates/core/src/effects_worker.rs index cb37d78..df2a98a 100644 --- a/crates/core/src/effects_worker.rs +++ b/crates/core/src/effects_worker.rs @@ -113,7 +113,10 @@ mod tests { impl Effects for Recorder { fn mute(&self, pid: u32, mute: bool) { - self.calls.lock().unwrap().push(format!("mute:{pid}:{mute}")); + self.calls + .lock() + .unwrap() + .push(format!("mute:{pid}:{mute}")); } fn suspend(&self, pid: u32, _enhanced: bool) { self.calls.lock().unwrap().push(format!("suspend:{pid}")); diff --git a/crates/core/src/hide.rs b/crates/core/src/hide.rs index 7c47b5b..fd5eba3 100644 --- a/crates/core/src/hide.rs +++ b/crates/core/src/hide.rs @@ -191,7 +191,7 @@ pub fn expand_descendants(roots: &[u32], edges: &[(u32, u32)]) -> Vec { /// [`HideController::commit_hide`] 原样执行,两段之间由调用方落盘意图。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct HidePlan { - /// 本次新增的隐藏目标(已剔除死句柄 / 已隐藏项 / 查不到 PID 的项)。 + /// 本次新增的隐藏目标(已剔除死句柄 / 不可见项 / 已隐藏项 / 查不到 PID 的项)。 pub fresh: Vec, /// 本次新增的静音进程。 pub mute: Vec, @@ -310,8 +310,10 @@ impl HideController { /// 与 PID 补查(仍查不到的目标剔除)。`freeze_pids` 为最终要冻结的 PID 集 /// (可能已含子进程树),仅在 `freeze_after_hide` 开启时生效。 /// - /// 隐藏是累加的,`show` 时一并恢复。已在隐藏 / 静音 / 冻结集内的目标会被 - /// 跳过——挂起是计数式的,重复施加会让解冻次数对不上。 + /// 只有「由本程序从可见变为不可见」的窗口才进入隐藏集,恢复即逆转这次 + /// 改变;本来就不可见的目标不入集。隐藏是累加的,`show` 时一并恢复。 + /// 已在隐藏 / 静音 / 冻结集内的目标会被跳过——挂起是计数式的,重复施加 + /// 会让解冻次数对不上。 pub fn plan_hide( &mut self, setting: &Setting, @@ -323,9 +325,12 @@ impl HideController { let mut fresh: Vec = Vec::new(); for t in targets { + // 只隐藏当前可见的窗口:本来就不可见的目标(如程序自行藏到托盘, + // Steam 的关闭按钮即是)不入集,恢复时也就不会被错误地弹出来。 if known.contains(&t.hwnd) || fresh.iter().any(|f| f.hwnd == t.hwnd) || !self.wm.is_window(t.hwnd) + || !self.wm.is_visible(t.hwnd) { continue; } @@ -992,7 +997,14 @@ mod tests { assert_eq!(*controller.effects.pauses.borrow(), 1, "应发送一次暂停键"); let outcome = controller.show(); - assert_eq!(outcome, ShowOutcome { shown: 1, stale: 0, refound: 0 }); + assert_eq!( + outcome, + ShowOutcome { + shown: 1, + stale: 0, + refound: 0 + } + ); assert!(!controller.is_hidden()); assert!(controller.wm.is_visible(10), "恢复后微信应可见"); assert_eq!(*controller.effects.resumes.borrow(), vec![10], "应解冻"); @@ -1212,7 +1224,14 @@ mod tests { ..Default::default() }); - assert_eq!(outcome, ShowOutcome { shown: 1, stale: 0, refound: 0 }); + assert_eq!( + outcome, + ShowOutcome { + shown: 1, + stale: 0, + refound: 0 + } + ); assert!(!controller.is_hidden(), "恢复完成后应回到未隐藏状态"); assert!(controller.wm.is_visible(10), "崩溃前隐藏的窗口应被找回"); assert_eq!(*controller.effects.resumes.borrow(), vec![10], "应解冻进程"); @@ -1280,7 +1299,11 @@ mod tests { let outcome = controller.show(); assert_eq!( outcome, - ShowOutcome { shown: 1, stale: 2, refound: 0 }, + ShowOutcome { + shown: 1, + stale: 2, + refound: 0 + }, "死句柄与被复用句柄都应跳过" ); assert!(!controller.wm.is_visible(20), "被复用的句柄不得被弹出来"); @@ -1354,11 +1377,23 @@ mod tests { }; let wm = MockWm::new(vec![win("微信", 10, "WeChat.exe", "C:\\WeChat.exe")], 0); let mut controller = HideController::new(wm, MockEffects::default()); - controller.apply_hide(&setting, &[Target::from_window(&win("微信", 10, "WeChat.exe", "C:\\WeChat.exe"))], &[]); + controller.apply_hide( + &setting, + &[Target::from_window(&win( + "微信", + 10, + "WeChat.exe", + "C:\\WeChat.exe", + ))], + &[], + ); assert!(controller.tracks_window(10)); assert!(controller.update_title(10, "微信 - 新会话")); - assert!(!controller.update_title(10, "微信 - 新会话"), "标题未变不算更新"); + assert!( + !controller.update_title(10, "微信 - 新会话"), + "标题未变不算更新" + ); assert!( !controller.update_title(10, bosskey_common::NO_TITLE), "NO_TITLE 不参与同步" @@ -1378,8 +1413,14 @@ mod tests { ]; assert!(sync_rule_titles(&mut rules, 10, "新标题")); assert_eq!(rules[0].title, "新标题"); - assert!(!sync_rule_titles(&mut rules, 10, "新标题"), "标题未变不算更新"); - assert!(!sync_rule_titles(&mut rules, 99, "别的"), "句柄不匹配不更新"); + assert!( + !sync_rule_titles(&mut rules, 10, "新标题"), + "标题未变不算更新" + ); + assert!( + !sync_rule_titles(&mut rules, 99, "别的"), + "句柄不匹配不更新" + ); assert!( !sync_rule_titles(&mut rules, 10, NO_TITLE), "NO_TITLE 不参与同步" @@ -1452,6 +1493,53 @@ mod tests { assert!(controller.wm.is_visible(99), "可见窗口不受影响"); } + #[test] + fn already_invisible_windows_are_not_recorded_or_restored() { + let setting = Setting { + hide_current: false, + mute_after_hide: true, + ..Setting::default() + }; + // Steam 用关闭按钮把自己藏了起来(窗口仍存在但不可见),记事本可见; + // 两者都被规则命中。 + let wm = MockWm::new( + vec![ + win_pid("Steam", 10, "steam.exe", 500, "C:\\steam.exe"), + win_pid("记事本", 20, "notepad.exe", 600, "C:\\notepad.exe"), + ], + 0, + ); + wm.hide(10); + let mut controller = HideController::new(wm, MockEffects::default()); + + controller.apply_hide( + &setting, + &[Target::bare(10, 500), Target::bare(20, 600)], + &[], + ); + assert_eq!(controller.hidden_count(), 1, "已不可见的窗口不应入集"); + assert_eq!( + *controller.effects.mutes.borrow(), + vec![(600, true)], + "只对真正被隐藏的窗口施加副作用" + ); + + let outcome = controller.show(); + assert_eq!( + outcome, + ShowOutcome { + shown: 1, + stale: 0, + refound: 0 + } + ); + assert!(controller.wm.is_visible(20), "记事本应恢复"); + assert!( + !controller.wm.is_visible(10), + "Steam 自己藏起来的窗口不得被恢复弹出" + ); + } + #[test] fn release_windows_frees_whole_process_and_shows_unknown_handles() { let setting = Setting { diff --git a/crates/core/src/i18n.rs b/crates/core/src/i18n.rs index 235c500..f602e41 100644 --- a/crates/core/src/i18n.rs +++ b/crates/core/src/i18n.rs @@ -85,7 +85,9 @@ impl Msg { Msg::ErrCoreExited => "核心已退出", Msg::ErrNotifyCore => "无法通知核心", Msg::ErrCoreTimeout => "核心响应超时", - Msg::ErrCoreExeMissing => "未找到核心程序 {exe}。它很可能被杀毒软件拦截或隔离了:请尝试将 Boss Key 的程序目录加入杀毒软件的白名单 / 信任区,再从隔离区恢复该文件;若无法恢复,请重新下载完整程序包。", + Msg::ErrCoreExeMissing => { + "未找到核心程序 {exe}。它很可能被杀毒软件拦截或隔离了:请尝试将 Boss Key 的程序目录加入杀毒软件的白名单 / 信任区,再从隔离区恢复该文件;若无法恢复,请重新下载完整程序包。" + } Msg::ErrFreezePartial => "{failed}/{total} 个进程冻结失败", Msg::ErrResumePartial => "{failed}/{total} 个进程解冻失败", Msg::ErrUrlSchemeNotAllowed => "只允许打开 http/https/mailto 链接", @@ -106,7 +108,9 @@ impl Msg { Msg::HiddenBody => "Windows hidden", Msg::ShownBody => "Windows restored", Msg::ConfigExeMissing => "Settings app not found", - Msg::RecoveryPersistFailedBody => "Cannot write the crash-recovery file; windows cannot be restored automatically after an abnormal exit", + Msg::RecoveryPersistFailedBody => { + "Cannot write the crash-recovery file; windows cannot be restored automatically after an abnormal exit" + } Msg::AutostartOffTitle => "Startup disabled", Msg::AutostartOffBody => "Boss Key will no longer start with Windows", Msg::AutostartOnTitle => "Startup enabled", @@ -123,7 +127,9 @@ impl Msg { Msg::ErrCoreExited => "The core has exited", Msg::ErrNotifyCore => "Cannot reach the core", Msg::ErrCoreTimeout => "The core did not respond in time", - Msg::ErrCoreExeMissing => "Core program {exe} not found. It was most likely blocked or quarantined by antivirus software: add the Boss Key program folder to your antivirus allowlist, then restore the file from quarantine; if that is not possible, download the full package again.", + Msg::ErrCoreExeMissing => { + "Core program {exe} not found. It was most likely blocked or quarantined by antivirus software: add the Boss Key program folder to your antivirus allowlist, then restore the file from quarantine; if that is not possible, download the full package again." + } Msg::ErrFreezePartial => "Failed to freeze {failed} of {total} processes", Msg::ErrResumePartial => "Failed to resume {failed} of {total} processes", Msg::ErrUrlSchemeNotAllowed => "Only http/https/mailto links may be opened", @@ -161,7 +167,9 @@ impl Msg { Msg::ErrCoreExited => "核心已結束", Msg::ErrNotifyCore => "無法通知核心", Msg::ErrCoreTimeout => "核心回應逾時", - Msg::ErrCoreExeMissing => "找不到核心程式 {exe}。它很可能被防毒軟體攔截或隔離了:請將 Boss Key 的程式資料夾加入防毒軟體的信任區/白名單,再從隔離區還原該檔案;若無法還原,請重新下載完整程式包。", + Msg::ErrCoreExeMissing => { + "找不到核心程式 {exe}。它很可能被防毒軟體攔截或隔離了:請將 Boss Key 的程式資料夾加入防毒軟體的信任區/白名單,再從隔離區還原該檔案;若無法還原,請重新下載完整程式包。" + } Msg::ErrFreezePartial => "{failed}/{total} 個程序凍結失敗", Msg::ErrResumePartial => "{failed}/{total} 個程序解除凍結失敗", Msg::ErrUrlSchemeNotAllowed => "僅允許開啟 http/https/mailto 連結", diff --git a/crates/core/src/logging.rs b/crates/core/src/logging.rs index bbcc9d9..77ec632 100644 --- a/crates/core/src/logging.rs +++ b/crates/core/src/logging.rs @@ -367,7 +367,10 @@ mod tests { let dir = temp_dir(); let logger = Logger::new(dir.clone(), 7); // 直接验证格式化结果(warn_at 走全局 logger,测试里用本地实例复刻其格式)。 - logger.log(Level::Warn, &format!("{} ({}:{})", "落盘失败", "agent.rs", 42)); + logger.log( + Level::Warn, + &format!("{} ({}:{})", "落盘失败", "agent.rs", 42), + ); let logs: Vec<_> = fs::read_dir(&dir).unwrap().flatten().collect(); let content = fs::read_to_string(logs[0].path()).unwrap(); assert!(content.contains("[WARN] 落盘失败 (agent.rs:42)")); diff --git a/crates/core/src/recovery.rs b/crates/core/src/recovery.rs index 7bf19a3..a0934b3 100644 --- a/crates/core/src/recovery.rs +++ b/crates/core/src/recovery.rs @@ -188,10 +188,7 @@ mod tests { std::fs::write(&path, "{ not valid json !!").unwrap(); assert_eq!(load(&path), None, "损坏文件应按无快照处理而非 panic"); assert!(!path.exists(), "损坏文件不应留在原名"); - assert!( - corrupt_path(&path).exists(), - "损坏文件应改名保留现场供排查" - ); + assert!(corrupt_path(&path).exists(), "损坏文件应改名保留现场供排查"); } #[test] diff --git a/crates/core/src/win_event.rs b/crates/core/src/win_event.rs index 7731e68..feaddf8 100644 --- a/crates/core/src/win_event.rs +++ b/crates/core/src/win_event.rs @@ -93,7 +93,9 @@ impl Drop for WinEventHook { #[cfg(test)] mod tests { use super::*; - use windows::Win32::UI::WindowsAndMessaging::{EVENT_OBJECT_FOCUS, EVENT_OBJECT_HIDE, OBJID_CURSOR}; + use windows::Win32::UI::WindowsAndMessaging::{ + EVENT_OBJECT_FOCUS, EVENT_OBJECT_HIDE, OBJID_CURSOR, + }; #[test] fn only_toplevel_destroy_show_namechange_are_relevant() { @@ -103,8 +105,14 @@ mod tests { assert!(relevant(EVENT_OBJECT_SHOW, obj, child)); assert!(relevant(EVENT_OBJECT_NAMECHANGE, obj, child)); assert!(!relevant(EVENT_OBJECT_HIDE, obj, child), "隐藏事件不用追踪"); - assert!(!relevant(EVENT_OBJECT_FOCUS, obj, child), "区间内的无关事件应过滤"); - assert!(!relevant(EVENT_OBJECT_DESTROY, OBJID_CURSOR.0, child), "非窗口对象应过滤"); + assert!( + !relevant(EVENT_OBJECT_FOCUS, obj, child), + "区间内的无关事件应过滤" + ); + assert!( + !relevant(EVENT_OBJECT_DESTROY, OBJID_CURSOR.0, child), + "非窗口对象应过滤" + ); assert!(!relevant(EVENT_OBJECT_DESTROY, obj, 3), "子对象事件应过滤"); } } diff --git a/crates/core/tests/agent_window_tracking.rs b/crates/core/tests/agent_window_tracking.rs index d662503..07c18ae 100644 --- a/crates/core/tests/agent_window_tracking.rs +++ b/crates/core/tests/agent_window_tracking.rs @@ -96,10 +96,7 @@ fn destroying_a_hidden_window_clears_the_record_in_real_time() { ); std::thread::sleep(Duration::from_millis(50)); } - assert!( - !recovery_path.exists(), - "记录清空后恢复文件应同步清除" - ); + assert!(!recovery_path.exists(), "记录清空后恢复文件应同步清除"); let quit = client.send(&Command::Quit).unwrap(); assert_eq!(quit, Response::Ok); diff --git a/docs/en/guide/faq.md b/docs/en/guide/faq.md index fe67efe..10c9526 100644 --- a/docs/en/guide/faq.md +++ b/docs/en/guide/faq.md @@ -17,6 +17,10 @@ Some stripped-down or older Windows builds (Windows 7 and earlier) may not inclu A settings window that will not open **does not affect the core's hiding features** — the core is a fully native program and does not depend on WebView2. You can still hide windows with your configured hotkeys. ::: +## Will restoring pop up programs I "closed" to the tray? + +No. Hiding only records windows that were **visible at the time**, and restoring only reverses Boss Key's own hiding; windows an app hid by itself (Steam's close button, for example, merely hides its window) are untouched and will not be shown on restore. + ## Antivirus flags or blocks Boss Key — what now? Boss Key listens for global hotkeys and hides windows, behaviour that antivirus software sometimes misreads. v3 uses a native single-file Rust implementation, which considerably reduces false positives. If it is still blocked: diff --git a/docs/guide/faq.md b/docs/guide/faq.md index 4a9986f..4f6aeab 100644 --- a/docs/guide/faq.md +++ b/docs/guide/faq.md @@ -17,6 +17,10 @@ v3 的配置程序(`config.exe`)使用 Tauri 编写,其运行依赖系统 配置界面无法打开**不影响核心的隐藏功能**——核心是纯原生程序,不依赖 WebView2。你仍可用已配置好的热键正常隐藏窗口。 ::: +## 恢复时会把已"关闭"到托盘的程序弹出来吗? + +不会。隐藏时只记录**当时可见**的窗口,恢复只逆转 Boss Key 自己的隐藏动作;程序自行藏到托盘的窗口(如 Steam 的关闭按钮只是隐藏窗口)不受影响,恢复时不会被弹出。 + ## 杀毒软件报毒 / 拦截怎么办? Boss Key 会监听全局热键、隐藏窗口,这类行为有时会被杀软误判。v3 已改用 Rust 原生单文件实现,显著降低了误报概率。若仍被拦截: diff --git a/docs/zh-tw/guide/faq.md b/docs/zh-tw/guide/faq.md index 0306cab..0fb1a5b 100644 --- a/docs/zh-tw/guide/faq.md +++ b/docs/zh-tw/guide/faq.md @@ -17,6 +17,10 @@ v3 的設定程式(`config.exe`)使用 Tauri 撰寫,其執行相依於系 設定介面無法開啟**不影響核心的隱藏功能**——核心是純原生程式,不相依於 WebView2。您仍可用已設定好的快速鍵正常隱藏視窗。 ::: +## 復原時會把已「關閉」到通知區域的程式彈出來嗎? + +不會。隱藏時只記錄**當時可見**的視窗,復原只逆轉 Boss Key 自己的隱藏動作;程式自行藏到通知區域的視窗(如 Steam 的關閉按鈕只是隱藏視窗)不受影響,復原時不會被彈出。 + ## 防毒軟體誤判/攔截怎麼辦? Boss Key 會監聽全域快速鍵、隱藏視窗,這類行為有時會被防毒軟體誤判。v3 已改用 Rust 原生單一檔案實作,顯著降低了誤判機率。若仍被攔截: From 27059644e88a2492371f9fa23527e2dd8675e335 Mon Sep 17 00:00:00 2001 From: Ivan Hanloth Date: Sun, 26 Jul 2026 15:27:55 +0100 Subject: [PATCH 17/19] =?UTF-8?q?chore:=20release=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AE=89=E5=85=A8=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 19 ++++++++++++++++++- docs/dev/release.md | 2 +- docs/en/dev/release.md | 2 +- docs/zh-tw/dev/release.md | 2 +- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 753f8fa..9ace456 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -154,12 +154,29 @@ jobs: # /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 ` # "dist/Boss-Key/Boss Key.exe" dist/Boss-Key/config.exe dist/installer/Boss-Key-*-Setup.exe + # 发布说明 = GitHub 自动生成的更新日志 + 结尾的安全提示。 + # 创建 Release 时若同时传 body 和 generate_release_notes,自定义内容只会被 + # 放在自动生成内容**之前**,所以这里先调 generate-notes API 拿到更新日志, + # 再手动把安全提示拼在末尾。 + - name: Compose release notes + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + $notes = (gh api "repos/$env:GITHUB_REPOSITORY/releases/generate-notes" -f tag_name="$env:TAG" --jq '.body') -join "`n" + $notice = @' + > [!WARNING] + > 【安全提示】 + > 本软件中运用了大量的系统底层API,并且提供开机自启、热键监控、窗口冻结等功能,容易被杀毒软件识别为木马病毒,但是程序本身完全开源、免费,并不包含病毒、木马行为,请放心使用。请务必从官方渠道下载和使用程序,运行程序前通过文件哈希、文件签名等方式验证文件的真实性,谨防利用软件信任伪装的病毒程序。 + '@ + Set-Content -Path release-notes.md -Value ($notes + "`n`n" + $notice) -Encoding utf8 + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: name: Boss Key ${{ env.TAG }} tag_name: ${{ env.TAG }} - generate_release_notes: true + body_path: release-notes.md draft: true prerelease: ${{ steps.version.outputs.is_prerelease == 'true' }} make_latest: ${{ steps.version.outputs.is_prerelease != 'true' }} diff --git a/docs/dev/release.md b/docs/dev/release.md index e545837..6eec938 100644 --- a/docs/dev/release.md +++ b/docs/dev/release.md @@ -99,7 +99,7 @@ GITHUB_TOKEN 创建的 PR 不会触发 `pull_request` 事件,PR 页面上不 **触发**:推送到 `main`(检测到有新的 `v*` tag 随之进入 `main` 的历史才继续),或手动触发并指定 tag。 -**做什么**:检测本次推送新带进 `main` 的 tag → 检出该 tag → 校验 tag 与代码版本一致 → 前端 / Rust 测试 → `package.ps1 -Installer` 组装 `dist/Boss-Key` 与 `dist/installer` → 把 `dist/Boss-Key` 压成便携 zip → 生成**构建来源证明**(Sigstore attestation)→ 创建**草稿** Release 并上传 zip 与安装包。 +**做什么**:检测本次推送新带进 `main` 的 tag → 检出该 tag → 校验 tag 与代码版本一致 → 前端 / Rust 测试 → `package.ps1 -Installer` 组装 `dist/Boss-Key` 与 `dist/installer` → 把 `dist/Boss-Key` 压成便携 zip → 生成**构建来源证明**(Sigstore attestation)→ 生成发布说明(自动生成的更新日志,末尾附安全提示)→ 创建**草稿** Release 并上传 zip 与安装包。 ::: info 为什么不监听 `push: tags` tag 是 `tag.yml` 用 GITHUB_TOKEN 推到 `dev` 的,那次推送不会触发任何工作流。而**合并 PR 并不产生 tag 推送事件**——tag 是独立的 ref,合并只是让它指向的提交变得可从 `main` 追溯。所以只能从 `main` 的 push 事件里检测。 diff --git a/docs/en/dev/release.md b/docs/en/dev/release.md index b0b55c2..5715a6f 100644 --- a/docs/en/dev/release.md +++ b/docs/en/dev/release.md @@ -99,7 +99,7 @@ A PR created with GITHUB_TOKEN does not raise a `pull_request` event, so the PR **Triggers**: a push to `main` (it continues only if a new `v*` tag entered `main`'s history with that push), or a manual run with an explicit tag. -**What it does**: detect the tag the push brought into `main` → check that tag out → verify the tag matches the code version → frontend / Rust tests → `package.ps1 -Installer` to assemble `dist/Boss-Key` and `dist/installer` → zip `dist/Boss-Key` as the portable archive → generate **build provenance** (a Sigstore attestation) → create a **draft** Release and upload the zip and the installer. +**What it does**: detect the tag the push brought into `main` → check that tag out → verify the tag matches the code version → frontend / Rust tests → `package.ps1 -Installer` to assemble `dist/Boss-Key` and `dist/installer` → zip `dist/Boss-Key` as the portable archive → generate **build provenance** (a Sigstore attestation) → compose the release notes (the auto-generated changelog with a security notice appended) → create a **draft** Release and upload the zip and the installer. ::: info Why it does not listen for `push: tags` The tag was pushed to `dev` by `tag.yml` using GITHUB_TOKEN, and that push triggers nothing. **Merging a PR does not produce a tag push event either** — a tag is an independent ref, and merging merely makes the commit it points at reachable from `main`. Detecting it from `main`'s push event is the only option left. diff --git a/docs/zh-tw/dev/release.md b/docs/zh-tw/dev/release.md index 45d7346..a61cc7d 100644 --- a/docs/zh-tw/dev/release.md +++ b/docs/zh-tw/dev/release.md @@ -99,7 +99,7 @@ GITHUB_TOKEN 建立的 PR 不會觸發 `pull_request` 事件,PR 頁面上不 **觸發**:推送到 `main`(偵測到有新的 `v*` tag 隨之進入 `main` 的歷史才繼續),或手動觸發並指定 tag。 -**做什麼**:偵測本次推送新帶進 `main` 的 tag → 檢出該 tag → 驗證 tag 與程式碼版本一致 → 前端/Rust 測試 → `package.ps1 -Installer` 組裝 `dist/Boss-Key` 與 `dist/installer` → 把 `dist/Boss-Key` 壓成可攜 zip → 產生**建置來源證明**(Sigstore attestation)→ 建立**草稿** Release 並上傳 zip 與安裝包。 +**做什麼**:偵測本次推送新帶進 `main` 的 tag → 檢出該 tag → 驗證 tag 與程式碼版本一致 → 前端/Rust 測試 → `package.ps1 -Installer` 組裝 `dist/Boss-Key` 與 `dist/installer` → 把 `dist/Boss-Key` 壓成可攜 zip → 產生**建置來源證明**(Sigstore attestation)→ 產生發布說明(自動產生的更新日誌,結尾附安全提示)→ 建立**草稿** Release 並上傳 zip 與安裝包。 ::: info 為什麼不監聽 `push: tags` tag 是 `tag.yml` 用 GITHUB_TOKEN 推到 `dev` 的,那次推送不會觸發任何工作流程。而**合併 PR 並不產生 tag 推送事件**——tag 是獨立的 ref,合併只是讓它指向的提交變得可從 `main` 追溯。所以只能從 `main` 的 push 事件裡偵測。 From 9e86f96a6e21c1d77b230681f24818e5df6e31a4 Mon Sep 17 00:00:00 2001 From: Ivan Hanloth Date: Sun, 26 Jul 2026 16:10:27 +0100 Subject: [PATCH 18/19] =?UTF-8?q?chore:=20=E4=BC=98=E5=8C=96=E6=96=87?= =?UTF-8?q?=E6=A1=88=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/config/ui/src/locales/en.js | 4 ++-- apps/config/ui/src/locales/zh-CN.js | 5 +++-- apps/config/ui/src/locales/zh-TW.js | 5 +++-- crates/core/src/tray.rs | 2 +- docs/dev/config-reference.md | 2 +- docs/dev/getting-started.md | 2 +- docs/en/dev/config-reference.md | 2 +- docs/en/guide/faq.md | 6 +++++- docs/en/guide/index.md | 2 +- docs/en/guide/notifications.md | 4 ++-- docs/en/guide/options.md | 13 +++++++++++-- docs/en/index.md | 2 +- docs/guide/faq.md | 6 +++++- docs/guide/index.md | 2 +- docs/guide/notifications.md | 4 ++-- docs/guide/options.md | 13 +++++++++++-- docs/index.md | 2 +- docs/zh-tw/dev/config-reference.md | 2 +- docs/zh-tw/dev/getting-started.md | 2 +- docs/zh-tw/guide/faq.md | 6 +++++- docs/zh-tw/guide/index.md | 2 +- docs/zh-tw/guide/notifications.md | 4 ++-- docs/zh-tw/guide/options.md | 13 +++++++++++-- docs/zh-tw/index.md | 2 +- 24 files changed, 74 insertions(+), 33 deletions(-) diff --git a/apps/config/ui/src/locales/en.js b/apps/config/ui/src/locales/en.js index 4a38e9f..9e04169 100644 --- a/apps/config/ui/src/locales/en.js +++ b/apps/config/ui/src/locales/en.js @@ -202,9 +202,9 @@ export default { "When the hotkey is pressed, also hide the foreground window in addition to the bound windows.", "options.clickToHide": "Toggle hiding by clicking the tray icon", "options.clickToHideDesc": "Left-click the tray icon to hide / show without pressing a hotkey.", - "options.hideIcon": "Also hide the tray icon", + "options.hideIcon": "Also hide Boss Key's tray icon", "options.hideIconDesc": - "Hide the tray icon along with the windows for more discretion; press the hotkey again to restore it.", + "Hide Boss Key's own tray icon along with the windows for more discretion; other programs' tray icons are not affected. Press the hotkey again to restore it.", "options.sendPause": "Send the pause key before hiding (beta)", "options.sendPauseDesc": "Send the media pause key before hiding (pausing any playing video or music); adds roughly 0.2 seconds of delay.", diff --git a/apps/config/ui/src/locales/zh-CN.js b/apps/config/ui/src/locales/zh-CN.js index b8afbaa..f0b3cac 100644 --- a/apps/config/ui/src/locales/zh-CN.js +++ b/apps/config/ui/src/locales/zh-CN.js @@ -195,8 +195,9 @@ export default { "options.hideCurrentDesc": "按下热键时,除已绑定窗口外,同时隐藏当前正在使用的前台窗口。", "options.clickToHide": "单击托盘图标切换隐藏", "options.clickToHideDesc": "左键单击托盘图标即可隐藏 / 显示,无需按热键。", - "options.hideIcon": "隐藏后同时隐藏托盘图标", - "options.hideIconDesc": "隐藏窗口时连托盘图标一起藏起,更隐蔽;再次触发热键可恢复。", + "options.hideIcon": "同时隐藏 Boss Key 托盘图标", + "options.hideIconDesc": + "隐藏窗口时连 Boss Key 自身的托盘图标一起藏起,更隐蔽;不影响其他程序的托盘图标。再次触发热键可恢复。", "options.sendPause": "隐藏前发送暂停键(Beta)", "options.sendPauseDesc": "隐藏前先发送媒体暂停键(暂停正在播放的视频 / 音乐),会带来约 0.2 秒延迟。", diff --git a/apps/config/ui/src/locales/zh-TW.js b/apps/config/ui/src/locales/zh-TW.js index 4ce2a6a..ca752e0 100644 --- a/apps/config/ui/src/locales/zh-TW.js +++ b/apps/config/ui/src/locales/zh-TW.js @@ -194,8 +194,9 @@ export default { "options.hideCurrentDesc": "按下快速鍵時,除已綁定的視窗外,同時隱藏目前正在使用的前景視窗。", "options.clickToHide": "按一下通知區域圖示切換隱藏", "options.clickToHideDesc": "以左鍵按一下通知區域圖示即可隱藏/顯示,不需按快速鍵。", - "options.hideIcon": "隱藏後一併隱藏通知區域圖示", - "options.hideIconDesc": "隱藏視窗時連通知區域圖示一起隱藏,更為隱密;再次觸發快速鍵即可復原。", + "options.hideIcon": "一併隱藏 Boss Key 通知區域圖示", + "options.hideIconDesc": + "隱藏視窗時連 Boss Key 自身的通知區域圖示一起隱藏,更為隱密;不影響其他程式的通知區域圖示。再次觸發快速鍵即可復原。", "options.sendPause": "隱藏前傳送暫停鍵(Beta)", "options.sendPauseDesc": "隱藏前先傳送媒體暫停鍵(暫停正在播放的影片/音樂),會造成約 0.2 秒延遲。", diff --git a/crates/core/src/tray.rs b/crates/core/src/tray.rs index aa334fc..d4e4e8c 100644 --- a/crates/core/src/tray.rs +++ b/crates/core/src/tray.rs @@ -101,7 +101,7 @@ fn load_icon_from_file() -> Option { pub struct TrayIcon { hwnd: HWND, - /// 业务层期望的可见性(如「隐藏后同时隐藏图标」时为 false)。 + /// 业务层期望的可见性(如「同时隐藏 Boss Key 托盘图标」时为 false)。 desired: bool, /// 图标是否实际已挂到任务栏(NIM_ADD 成功)。 visible: bool, diff --git a/docs/dev/config-reference.md b/docs/dev/config-reference.md index e733bdb..de4fa0c 100644 --- a/docs/dev/config-reference.md +++ b/docs/dev/config-reference.md @@ -48,7 +48,7 @@ Boss Key 的配置保存在与可执行文件**同目录**的 `config.json` 中 | `send_before_hide` | bool | `false` | [隐藏前发送暂停键](/guide/options#隐藏前发送暂停键-beta) | | `hide_current` | bool | `true` | [同时隐藏当前活动窗口](/guide/options#同时隐藏当前活动窗口) | | `click_to_hide` | bool | `true` | [单击托盘切换隐藏](/guide/options#单击托盘图标切换隐藏) | -| `hide_icon_after_hide` | bool | `false` | [隐藏后同时隐藏托盘图标](/guide/options#隐藏后同时隐藏托盘图标) | +| `hide_icon_after_hide` | bool | `false` | [同时隐藏 Boss Key 托盘图标](/guide/options#同时隐藏-boss-key-托盘图标) | | `tray_badges` | object | 见下 | [图标状态提示](/guide/notifications#图标状态提示) | | `tray_show_tooltip` | bool | `true` | [显示图标悬浮名称](/guide/notifications#显示图标悬浮名称) | | `freeze_after_hide` | bool | `false` | [进程冻结总开关](/guide/freeze#隐藏窗口时冻结进程) | diff --git a/docs/dev/getting-started.md b/docs/dev/getting-started.md index 15d331f..8fbba0b 100644 --- a/docs/dev/getting-started.md +++ b/docs/dev/getting-started.md @@ -12,7 +12,7 @@ title: 本地运行 | --- | --- | --- | | **Rust** | stable,建议 1.85+(项目使用 edition 2024) | | | **Node.js** | 18+,建议 24(用于前端构建) | | -| **WebView2** | 配置界面运行时(Win10/11 通常已内置) | 系统自带 / [微软官网](https://developer.microsoft.com/microsoft-edge/webview2) | +| **WebView2** | 配置界面运行时(Win10/11 通常已内置) | 系统自带 / [微软官网](https://developer.microsoft.com/zh-cn/microsoft-edge/webview2) | | **Inno Setup 6** | *可选*,本机生成安装包时需要 | `winget install JRSoftware.InnoSetup` | | **pssuspend64.exe** | *可选*,测试增强冻结时需要 | [Microsoft PSTools](https://download.sysinternals.com/files/PSTools.zip) | diff --git a/docs/en/dev/config-reference.md b/docs/en/dev/config-reference.md index 76f8b69..839c1ff 100644 --- a/docs/en/dev/config-reference.md +++ b/docs/en/dev/config-reference.md @@ -48,7 +48,7 @@ The settings window reads and writes the configuration automatically. This page | `send_before_hide` | bool | `false` | [Send the pause key before hiding](/en/guide/options) | | `hide_current` | bool | `true` | [Also hide the active window](/en/guide/options) | | `click_to_hide` | bool | `true` | [Toggle hiding by clicking the tray icon](/en/guide/options) | -| `hide_icon_after_hide` | bool | `false` | [Also hide the tray icon](/en/guide/options) | +| `hide_icon_after_hide` | bool | `false` | [Also hide Boss Key's tray icon](/en/guide/options) | | `tray_badges` | object | See below | [Tray icon status](/en/guide/notifications#tray-icon-status) | | `tray_show_tooltip` | bool | `true` | [Tray icon tooltip](/en/guide/notifications#tray-icon-tooltip) | | `freeze_after_hide` | bool | `false` | [Freezing master switch](/en/guide/freeze) | diff --git a/docs/en/guide/faq.md b/docs/en/guide/faq.md index 10c9526..2c9992d 100644 --- a/docs/en/guide/faq.md +++ b/docs/en/guide/faq.md @@ -43,7 +43,11 @@ Check in order: ## A window was hidden and won't come back -Use the [window recovery tool](/en/guide/recovery) (Options → Tools) to tick and restore it. If you enabled "Also hide the tray icon", use your **restore hotkey**. +Use the [window recovery tool](/en/guide/recovery) (Options → Tools) to tick and restore it. If you enabled "Also hide Boss Key's tray icon", use your **restore hotkey**. + +## Can Boss Key hide other programs' tray icons? + +Boss Key can only hide [its own tray icon](/en/guide/options); it cannot touch tray icons owned by other programs. Windows provides this itself: for detailed steps see Microsoft's guide [Customize the taskbar in Windows · System tray](https://support.microsoft.com/en-us/windows/experience/personalization/customize-the-taskbar-in-windows#system-tray), or open the [taskbar settings](ms-settings:taskbar) directly (the link works only on Windows) and choose which icons appear in the taskbar corner. ## The enhanced freezing switch is greyed out diff --git a/docs/en/guide/index.md b/docs/en/guide/index.md index 8fb0f03..8d0fdb0 100644 --- a/docs/en/guide/index.md +++ b/docs/en/guide/index.md @@ -12,7 +12,7 @@ title: Introduction - **Multi-window / multi-process hiding**: hide any number of windows at once, or hide every window of a program by process. - **Multiple triggers**: global keyboard hotkeys, middle / side mouse button clicks (optionally with modifier keys), fast movement into a screen corner, and auto-hide on idle. -- **Hiding enhancements**: mute automatically after hiding, send the media pause key, freeze processes to cut CPU and memory usage, and hide the tray icon along with the windows. +- **Hiding enhancements**: mute automatically after hiding, send the media pause key, freeze processes to cut CPU and memory usage, and hide Boss Key's own tray icon along with the windows. - **Precise matching**: target a single window, or match titles and process paths in bulk with **regular expressions**. - **Minimal footprint**: v3 is rewritten in Rust; the resident core is roughly a 350 KB binary using about 1 MB of memory. - **Reliable**: crash logs, crash recovery and a watchdog form three layers of defence against windows disappearing permanently. diff --git a/docs/en/guide/notifications.md b/docs/en/guide/notifications.md index 7371368..51bf5fc 100644 --- a/docs/en/guide/notifications.md +++ b/docs/en/guide/notifications.md @@ -45,6 +45,6 @@ Hovering over the tray icon shows "Boss Key" by default. Turn this option off to - **Hide / show notifications are off by default**: a notification on every hide would rather defeat the purpose, so they are not shown. Turn them on if you want explicit feedback. - **Start / exit / startup notifications are on by default**: these are infrequent events, and keeping them helps you confirm the core's state. -::: info Not the same as "Also hide the tray icon" -[Also hide the tray icon](/en/guide/options) controls whether **the icon itself is visible**; this page controls **notifications and icon badges**. They are independent. +::: info Not the same as "Also hide Boss Key's tray icon" +[Also hide Boss Key's tray icon](/en/guide/options) controls whether **the icon itself is visible**; this page controls **notifications and icon badges**. They are independent. ::: diff --git a/docs/en/guide/options.md b/docs/en/guide/options.md index ccb73b1..2aefba7 100644 --- a/docs/en/guide/options.md +++ b/docs/en/guide/options.md @@ -30,16 +30,21 @@ When on, a **left click on the tray icon** hides / shows the windows. - **On** by default. -### Also hide the tray icon +### Also hide Boss Key's tray icon -When on, hiding the windows **hides Boss Key's tray icon as well** for extra discretion; triggering restore brings back both the windows and the icon. +When on, hiding the windows **hides Boss Key's own tray icon as well** for extra discretion; triggering restore brings back both the windows and the icon. - **Off** by default. +- This option only affects Boss Key's own tray icon — it **does not hide other programs' tray icons** (including the icons of the programs whose windows are hidden). ::: warning Once the tray icon is hidden you cannot click it to restore or open the settings. Be sure to remember your **hide / show hotkey** or mouse gesture, and use that to restore. ::: +::: tip Hiding other programs' tray icons +Windows itself controls which tray icons are visible: you can choose which icons appear in the taskbar corner by hand. For detailed steps see Microsoft's guide [Customize the taskbar in Windows · System tray](https://support.microsoft.com/en-us/windows/experience/personalization/customize-the-taskbar-in-windows#system-tray), or open the [taskbar settings](ms-settings:taskbar) directly (the `ms-settings:taskbar` link works only on Windows; the browser asks for confirmation first). +::: + ### Send the pause key before hiding (beta) When on, Boss Key sends the **media pause key** to the window **before** hiding it, to try to pause any video or music playing inside. @@ -93,3 +98,7 @@ When something goes wrong, the logs in the `logs` folder are the primary source ### Window recovery tool For **recovering windows that were hidden by mistake and cannot be restored with a hotkey**. Click "Open" to list every window, then tick the ones to restore. See [Window recovery & crash self-healing](/en/guide/recovery). + +## See also + +[Customize the taskbar in Windows – Microsoft Support](https://support.microsoft.com/en-us/windows/experience/personalization/customize-the-taskbar-in-windows#system-tray) diff --git a/docs/en/index.md b/docs/en/index.md index 08e723e..cd8d348 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -26,7 +26,7 @@ features: details: v3 is rewritten in Rust; the resident core uses about 1 MB of memory, and its native implementation rarely trips antivirus false positives. - title: Highly configurable icon: 💅 - details: Mute after hiding, send a pause key, freeze processes, hide the tray icon, match windows and processes by regex — tailor it to how you work. + details: Mute after hiding, send a pause key, freeze processes, hide its own tray icon, match windows and processes by regex — tailor it to how you work. - title: Built to stay up icon: 🛡️ details: Crash logs, crash recovery and scheduled-task startup form three layers of defence for long-running background operation. diff --git a/docs/guide/faq.md b/docs/guide/faq.md index 4f6aeab..876de2d 100644 --- a/docs/guide/faq.md +++ b/docs/guide/faq.md @@ -43,7 +43,11 @@ Boss Key 会监听全局热键、隐藏窗口,这类行为有时会被杀软 ## 窗口被隐藏后显示不回来了? -使用 [窗口恢复工具](/guide/recovery#窗口恢复工具)(其他选项 → 工具)勾选并恢复。若开启了"隐藏托盘图标",请用你的**恢复热键**恢复。 +使用 [窗口恢复工具](/guide/recovery#窗口恢复工具)(其他选项 → 工具)勾选并恢复。若开启了"同时隐藏 Boss Key 托盘图标",请用你的**恢复热键**恢复。 + +## 能隐藏其他程序的托盘图标吗? + +Boss Key 只能隐藏[自身的托盘图标](/guide/options#同时隐藏-boss-key-托盘图标),无法操作其他程序的托盘图标。可使用 Windows 自带的功能手动设置:具体步骤参见微软官方教程 [在 Windows 中自定义任务栏 · 系统托盘](https://support.microsoft.com/zh-cn/windows/experience/personalization/customize-the-taskbar-in-windows#system-tray),或直接打开 [任务栏设置](ms-settings:taskbar)(该链接仅在 Windows 上有效),选择哪些图标显示在任务栏角落。 ## 增强冻结的开关是灰的,点不了? diff --git a/docs/guide/index.md b/docs/guide/index.md index a8fd9ac..94b01a0 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -12,7 +12,7 @@ title: 简介 - **多窗口 / 多进程隐藏**:可同时隐藏任意数量的窗口,或按进程隐藏某个程序的全部窗口。 - **多种触发方式**:键盘全局热键、鼠标中键 / 侧键连击(可搭配修饰键)、快速移动到屏幕四角、空闲自动隐藏。 -- **隐藏增强**:隐藏后自动静音、发送媒体暂停键、冻结进程以降低 CPU / 内存占用、连托盘图标一起隐藏。 +- **隐藏增强**:隐藏后自动静音、发送媒体暂停键、冻结进程以降低 CPU / 内存占用、连 Boss Key 自身托盘图标一起隐藏。 - **精细匹配**:既能锁定单个窗口,也能用**正则表达式**批量匹配标题或进程路径。 - **极低占用**:v3 使用 Rust 重写,核心常驻仅约 350 KB 二进制、约 1 MB 内存。 - **稳定可靠**:崩溃日志、崩溃恢复、看门狗三层防线,避免窗口"永久消失"。 diff --git a/docs/guide/notifications.md b/docs/guide/notifications.md index 1d62659..f26495c 100644 --- a/docs/guide/notifications.md +++ b/docs/guide/notifications.md @@ -45,6 +45,6 @@ Boss Key 通过**托盘气泡通知**与**托盘图标角标**两类提示反映 - **隐藏 / 显示通知默认关闭**:因为摸鱼场景下,每次隐藏都弹通知反而容易"暴露",因此默认不提示。若你希望有明确反馈可自行开启。 - **启动 / 退出 / 自启通知默认开启**:这些是低频事件,保留提示有助于你确认核心状态。 -::: info 与"隐藏托盘图标"的区别 -[隐藏后同时隐藏托盘图标](/guide/options#隐藏后同时隐藏托盘图标) 控制的是**图标本身是否可见**;本页控制的是**气泡通知与图标角标**,三者相互独立。 +::: info 与"隐藏 Boss Key 托盘图标"的区别 +[同时隐藏 Boss Key 托盘图标](/guide/options#同时隐藏-boss-key-托盘图标) 控制的是**图标本身是否可见**;本页控制的是**气泡通知与图标角标**,三者相互独立。 ::: diff --git a/docs/guide/options.md b/docs/guide/options.md index 1bb8c70..94e5298 100644 --- a/docs/guide/options.md +++ b/docs/guide/options.md @@ -30,16 +30,21 @@ title: 其他选项 - 默认**开启**。 -### 隐藏后同时隐藏托盘图标 +### 同时隐藏 Boss Key 托盘图标 -开启后,隐藏窗口时会**连 Boss Key 的托盘图标一起隐藏**,更加隐蔽;再次触发恢复即可恢复窗口与图标。 +开启后,隐藏窗口时会**连 Boss Key 自身的托盘图标一起隐藏**,更加隐蔽;再次触发恢复即可恢复窗口与图标。 - 默认**关闭**。 +- 该选项只作用于 Boss Key 自己的托盘图标,**不会隐藏其他程序的托盘图标**(包括被隐藏窗口所属程序的图标)。 ::: warning 托盘图标被隐藏后,你将无法通过点击图标来恢复或打开设置。请务必记住你的**隐藏 / 显示热键**或鼠标手势,用它来恢复。 ::: +::: tip 隐藏其他程序的托盘图标 +Windows 自带控制托盘图标显隐的功能,可手动设置哪些程序的图标显示在任务栏角落,具体步骤参见微软官方教程 [在 Windows 中自定义任务栏 · 系统托盘](https://support.microsoft.com/zh-cn/windows/experience/personalization/customize-the-taskbar-in-windows#system-tray),或直接打开 [任务栏设置](ms-settings:taskbar)(`ms-settings:taskbar` 链接仅在 Windows 上有效,浏览器会先请求确认)。 +::: + ### 隐藏前发送暂停键(Beta) 开启后,隐藏窗口**前**会先向窗口发送**媒体暂停键**,尝试暂停其中正在播放的视频 / 音乐。 @@ -93,3 +98,7 @@ title: 其他选项 ### 窗口恢复工具 用于**找回被误隐藏、无法通过热键恢复的窗口**。点击"打开"后会列出所有窗口,勾选后即可恢复显示。详见 [窗口恢复与崩溃自愈](/guide/recovery#窗口恢复工具)。 + +## See Also + +[在 Windows 中自定义任务栏 - Microsoft 支持](https://support.microsoft.com/zh-cn/windows/experience/personalization/customize-the-taskbar-in-windows#system-tray) diff --git a/docs/index.md b/docs/index.md index 5018f09..daf1a44 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,7 +26,7 @@ features: details: v3 使用 Rust 重写,核心常驻后台内存占用仅约 1 MB,原生实现不易被杀软误报。 - title: 超多姿势 icon: 💅 - details: 隐藏后静音、发送暂停键、冻结进程、隐藏托盘图标、正则匹配窗口/进程……高度可定制,满足不同摸鱼需求。 + details: 隐藏后静音、发送暂停键、冻结进程、隐藏自身托盘图标、正则匹配窗口/进程……高度可定制,满足不同摸鱼需求。 - title: 稳如磐石 icon: 🛡️ details: 崩溃日志、崩溃恢复、计划任务三层防线,常驻后台稳定运行。 diff --git a/docs/zh-tw/dev/config-reference.md b/docs/zh-tw/dev/config-reference.md index a4ae92f..ce0312e 100644 --- a/docs/zh-tw/dev/config-reference.md +++ b/docs/zh-tw/dev/config-reference.md @@ -48,7 +48,7 @@ Boss Key 的設定儲存在與執行檔**同資料夾**的 `config.json` 中。* | `send_before_hide` | bool | `false` | [隱藏前傳送暫停鍵](/zh-tw/guide/options) | | `hide_current` | bool | `true` | [同時隱藏目前使用中的視窗](/zh-tw/guide/options) | | `click_to_hide` | bool | `true` | [按一下通知區域圖示切換隱藏](/zh-tw/guide/options) | -| `hide_icon_after_hide` | bool | `false` | [隱藏後一併隱藏通知區域圖示](/zh-tw/guide/options) | +| `hide_icon_after_hide` | bool | `false` | [一併隱藏 Boss Key 通知區域圖示](/zh-tw/guide/options) | | `tray_badges` | object | 見下 | [圖示狀態提示](/zh-tw/guide/notifications#圖示狀態提示) | | `tray_show_tooltip` | bool | `true` | [顯示圖示懸浮名稱](/zh-tw/guide/notifications#顯示圖示懸浮名稱) | | `freeze_after_hide` | bool | `false` | [程序凍結總開關](/zh-tw/guide/freeze) | diff --git a/docs/zh-tw/dev/getting-started.md b/docs/zh-tw/dev/getting-started.md index 48d5d6e..a57302d 100644 --- a/docs/zh-tw/dev/getting-started.md +++ b/docs/zh-tw/dev/getting-started.md @@ -12,7 +12,7 @@ title: 本機執行 | --- | --- | --- | | **Rust** | stable,建議 1.85+(專案使用 edition 2024) | | | **Node.js** | 18+,建議 24(用於前端建置) | | -| **WebView2** | 設定介面執行階段(Win10/11 通常已內建) | 系統內建/[微軟官網](https://developer.microsoft.com/microsoft-edge/webview2) | +| **WebView2** | 設定介面執行階段(Win10/11 通常已內建) | 系統內建/[微軟官網](https://developer.microsoft.com/zh-tw/microsoft-edge/webview2) | | **Inno Setup 6** | *選用*,本機產生安裝包時需要 | `winget install JRSoftware.InnoSetup` | | **pssuspend64.exe** | *選用*,測試增強凍結時需要 | [Microsoft PSTools](https://download.sysinternals.com/files/PSTools.zip) | diff --git a/docs/zh-tw/guide/faq.md b/docs/zh-tw/guide/faq.md index 0fb1a5b..a726337 100644 --- a/docs/zh-tw/guide/faq.md +++ b/docs/zh-tw/guide/faq.md @@ -43,7 +43,11 @@ Boss Key 會監聽全域快速鍵、隱藏視窗,這類行為有時會被防 ## 視窗被隱藏後顯示不回來了? -使用 [視窗復原工具](/zh-tw/guide/recovery)(其他選項 → 工具)勾選並復原。若開啟了「隱藏通知區域圖示」,請用您的**復原快速鍵**復原。 +使用 [視窗復原工具](/zh-tw/guide/recovery)(其他選項 → 工具)勾選並復原。若開啟了「一併隱藏 Boss Key 通知區域圖示」,請用您的**復原快速鍵**復原。 + +## 能隱藏其他程式的通知區域圖示嗎? + +Boss Key 只能隱藏[自身的通知區域圖示](/zh-tw/guide/options),無法操作其他程式的通知區域圖示。可使用 Windows 內建的功能手動設定:詳細步驟參見微軟官方教學 [在 Windows 中自訂工作列 · 系統匣](https://support.microsoft.com/zh-tw/windows/experience/personalization/customize-the-taskbar-in-windows#system-tray),或直接開啟 [工作列設定](ms-settings:taskbar)(該連結僅在 Windows 上有效),選擇哪些圖示顯示在工作列角落。 ## 增強凍結的開關是灰的,按不了? diff --git a/docs/zh-tw/guide/index.md b/docs/zh-tw/guide/index.md index 36a3119..bf43389 100644 --- a/docs/zh-tw/guide/index.md +++ b/docs/zh-tw/guide/index.md @@ -12,7 +12,7 @@ title: 簡介 - **多視窗/多程序隱藏**:可同時隱藏任意數量的視窗,或按程序隱藏某個程式的全部視窗。 - **多種觸發方式**:鍵盤全域快速鍵、滑鼠中鍵/側鍵連按(可搭配輔助按鍵)、快速移動到螢幕四角、閒置自動隱藏。 -- **隱藏增強**:隱藏後自動靜音、傳送媒體暫停鍵、凍結程序以降低 CPU/記憶體佔用、連通知區域圖示一起隱藏。 +- **隱藏增強**:隱藏後自動靜音、傳送媒體暫停鍵、凍結程序以降低 CPU/記憶體佔用、連 Boss Key 自身通知區域圖示一起隱藏。 - **精細比對**:既能鎖定單一視窗,也能用**正規表示式**批次比對標題或程序路徑。 - **極低佔用**:v3 以 Rust 重寫,核心常駐僅約 350 KB 二進位檔、約 1 MB 記憶體。 - **穩定可靠**:當機記錄、當機復原、監控程式三層防線,避免視窗「永久消失」。 diff --git a/docs/zh-tw/guide/notifications.md b/docs/zh-tw/guide/notifications.md index 837f00c..ee3826f 100644 --- a/docs/zh-tw/guide/notifications.md +++ b/docs/zh-tw/guide/notifications.md @@ -45,6 +45,6 @@ Boss Key 透過**通知區域氣泡通知**與**圖示角標**兩類提示反映 - **隱藏/顯示通知預設關閉**:因為摸魚情境下,每次隱藏都顯示通知反而容易「曝光」,因此預設不提示。若您希望有明確回饋可自行開啟。 - **啟動/結束/自動啟動通知預設開啟**:這些是低頻事件,保留提示有助於您確認核心狀態。 -::: info 與「隱藏通知區域圖示」的區別 -[隱藏後一併隱藏通知區域圖示](/zh-tw/guide/options) 控制的是**圖示本身是否可見**;本頁控制的是**氣泡通知與圖示角標**,三者相互獨立。 +::: info 與「隱藏 Boss Key 通知區域圖示」的區別 +[一併隱藏 Boss Key 通知區域圖示](/zh-tw/guide/options) 控制的是**圖示本身是否可見**;本頁控制的是**氣泡通知與圖示角標**,三者相互獨立。 ::: diff --git a/docs/zh-tw/guide/options.md b/docs/zh-tw/guide/options.md index f9f06b2..81293f8 100644 --- a/docs/zh-tw/guide/options.md +++ b/docs/zh-tw/guide/options.md @@ -30,16 +30,21 @@ title: 其他選項 - 預設**開啟**。 -### 隱藏後一併隱藏通知區域圖示 +### 一併隱藏 Boss Key 通知區域圖示 -開啟後,隱藏視窗時會**連 Boss Key 的通知區域圖示一起隱藏**,更加隱密;再次觸發復原即可復原視窗與圖示。 +開啟後,隱藏視窗時會**連 Boss Key 自身的通知區域圖示一起隱藏**,更加隱密;再次觸發復原即可復原視窗與圖示。 - 預設**關閉**。 +- 該選項只作用於 Boss Key 自己的通知區域圖示,**不會隱藏其他程式的通知區域圖示**(包括被隱藏視窗所屬程式的圖示)。 ::: warning 通知區域圖示被隱藏後,您將無法透過按圖示來復原或開啟設定。請務必記住您的**隱藏/顯示快速鍵**或滑鼠手勢,用它來復原。 ::: +::: tip 隱藏其他程式的通知區域圖示 +Windows 內建控制通知區域圖示顯示與否的功能,可手動設定哪些程式的圖示顯示在工作列角落,詳細步驟參見微軟官方教學 [在 Windows 中自訂工作列 · 系統匣](https://support.microsoft.com/zh-tw/windows/experience/personalization/customize-the-taskbar-in-windows#system-tray),或直接開啟 [工作列設定](ms-settings:taskbar)(`ms-settings:taskbar` 連結僅在 Windows 上有效,瀏覽器會先要求確認)。 +::: + ### 隱藏前傳送暫停鍵(Beta) 開啟後,隱藏視窗**前**會先向視窗傳送**媒體暫停鍵**,嘗試暫停其中正在播放的影片/音樂。 @@ -93,3 +98,7 @@ title: 其他選項 ### 視窗復原工具 用於**找回被誤隱藏、無法透過快速鍵復原的視窗**。按一下「開啟」後會列出所有視窗,勾選後即可復原顯示。詳見 [視窗復原與當機自癒](/zh-tw/guide/recovery)。 + +## 延伸閱讀 + +[在 Windows 中自訂工作列 - Microsoft 支援](https://support.microsoft.com/zh-tw/windows/experience/personalization/customize-the-taskbar-in-windows#system-tray) diff --git a/docs/zh-tw/index.md b/docs/zh-tw/index.md index 0bfc4a3..2faa7a2 100644 --- a/docs/zh-tw/index.md +++ b/docs/zh-tw/index.md @@ -26,7 +26,7 @@ features: details: v3 以 Rust 重寫,核心常駐背景的記憶體佔用僅約 1 MB,原生實作不易被防毒軟體誤判。 - title: 超多姿勢 icon: 💅 - details: 隱藏後靜音、傳送暫停鍵、凍結程序、隱藏通知區域圖示、正規表示式比對視窗/程序……高度可自訂,滿足不同摸魚需求。 + details: 隱藏後靜音、傳送暫停鍵、凍結程序、隱藏自身通知區域圖示、正規表示式比對視窗/程序……高度可自訂,滿足不同摸魚需求。 - title: 穩如磐石 icon: 🛡️ details: 當機記錄、當機復原、排程工作三層防線,常駐背景穩定執行。 From 60d1e3eb2ffaee227429bf54e28651555ac70384 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 26 Jul 2026 16:12:41 +0000 Subject: [PATCH 19/19] chore(release): v3.1.0-rc.1 --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- apps/config/src-tauri/tauri.conf.json | 2 +- apps/config/ui/package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f2f960..fbabbd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -288,7 +288,7 @@ dependencies = [ [[package]] name = "bosskey-common" -version = "3.0.0" +version = "3.1.0-rc.1" dependencies = [ "regex", "serde", @@ -299,7 +299,7 @@ dependencies = [ [[package]] name = "bosskey-config" -version = "3.0.0" +version = "3.1.0-rc.1" dependencies = [ "bosskey-common", "bosskey-core", @@ -314,7 +314,7 @@ dependencies = [ [[package]] name = "bosskey-core" -version = "3.0.0" +version = "3.1.0-rc.1" dependencies = [ "bosskey-common", "serde", diff --git a/Cargo.toml b/Cargo.toml index 112262b..1a04cbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["crates/common", "crates/core", "apps/config/src-tauri"] [workspace.package] -version = "3.0.0" +version = "3.1.0-rc.1" edition = "2024" authors = ["IvanHanloth"] license = "MIT" diff --git a/apps/config/src-tauri/tauri.conf.json b/apps/config/src-tauri/tauri.conf.json index 7d17215..aacf6f5 100644 --- a/apps/config/src-tauri/tauri.conf.json +++ b/apps/config/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Boss Key", - "version": "3.0.0", + "version": "3.1.0-rc.1", "identifier": "cn.hanloth.bosskey.config", "build": { "frontendDist": "../dist", diff --git a/apps/config/ui/package.json b/apps/config/ui/package.json index 38c325f..61ca19b 100644 --- a/apps/config/ui/package.json +++ b/apps/config/ui/package.json @@ -1,7 +1,7 @@ { "name": "bosskey-config-ui", "private": true, - "version": "3.0.0", + "version": "3.1.0-rc.1", "type": "module", "scripts": { "dev": "vite",