feat: 方法级代码审查 + 模板导入/预览增强 + SQLFluff 方言 + AI 空响应报错修复

- 方法级审查:CodeLens 触发 + 单次 AI 调用(规则匹配 + 6 维度深度审查),新增 method-extractor / status-cache / codeLensProvider
- 模板导入:severity 保留原始值 + 占位 id、去重对照统一 known-rules、重复提示条双语翻译、箭头展开/折叠 UI、520 条静态规则补 zh/ja 翻译
- SQL:sql-lint 重命名 sqlfluff + sqlfluff.dialect 方言可配置 + 默认方言调整
- ESLint:v9 flat config 接线修复(overrideConfigFile)+ legacy 迁移提示
- AI:空响应 EmptyContentError + 重试一次 + max_tokens 截断专用报错
- JSP:整文件检查走 PMD JSP 规则集 + scriptlet 包装解析 + 行号映射
- 诊断按 severity + 行号排序
This commit is contained in:
范智鹏
2026-08-03 22:53:20 +08:00
parent 5848eaa82a
commit 247f19fd44
46 changed files with 6143 additions and 937 deletions
+72 -2
View File
@@ -1,20 +1,26 @@
import * as vscode from 'vscode';
import { Orchestrator } from '../orchestrator/orchestrator';
import { runAIReview } from '../ai/engine';
import { runAIReview, runMethodReview } from '../ai/engine';
import { loadActiveRules } from '../rules/yaml-parser';
import { filterAndSummarize } from '../rules/rule-filter';
import { filterAndSummarize, filterForDocument } from '../rules/rule-filter';
import { mergeResults, MergedReport } from '../merger/merger';
import { reportToMarkdown } from '../utils/report';
import { getApiKey } from '../config';
import { ReviewPanel } from '../panel/webview';
import { t } from '../i18n/messages';
import { exportTemplate } from '../rules/export-service';
import { extractMethodScope } from '../scope/method-extractor';
import { ReviewStatusCache } from '../scope/status-cache';
import { MethodCodeLensProvider } from '../views/codeLensProvider';
import type { CustomRule } from '../types';
let currentReport: MergedReport | null = null;
export function registerCommands(
context: vscode.ExtensionContext,
orchestrator: Orchestrator,
codeLensProvider: MethodCodeLensProvider,
statusCache: ReviewStatusCache,
): void {
context.subscriptions.push(
@@ -105,6 +111,70 @@ export function registerCommands(
})
);
context.subscriptions.push(
vscode.commands.registerCommand('codeReviewer.reviewMethod', async (symbolRange?: vscode.Range) => {
const editor = vscode.window.activeTextEditor;
if (!editor) { return; }
const document = editor.document;
const workspaceRoot = vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath;
const targetRange = symbolRange ?? new vscode.Range(editor.selection.active, editor.selection.active);
const scope = await extractMethodScope(document, targetRange);
if (!scope) {
vscode.window.showWarningMessage(t('methodReview.noMethod'));
return;
}
let customRules: CustomRule[] = [];
if (workspaceRoot) {
const allRules = loadActiveRules(workspaceRoot);
customRules = filterForDocument(allRules, document);
}
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: t('methodReview.running', { 0: scope.name }),
cancellable: false,
}, async () => {
const result = await runMethodReview(context, scope, customRules);
const methodLine = scope.range.start.line;
const totalIssues = result.customRuleResults.length + result.findings.length;
currentReport = mergeResults({
staticDiagnostics: [],
customRuleResults: result.customRuleResults.map(r => ({
...r,
line: r.line + methodLine,
})),
translatedDiagnostics: [],
aiFindings: result.findings.map(f => ({
...f,
line: f.line + methodLine - 1,
})),
errors: result.error ? [result.error] : [],
degraded: result.degraded,
startTime: Date.now(),
filePath: document.uri.fsPath,
language: document.languageId,
adapterIds: [],
customRuleFilterInfo: undefined,
});
statusCache.set(document.uri, scope.name, totalIssues);
codeLensProvider.refresh();
const panel = ReviewPanel.createOrShow(context.extensionUri);
panel.update(currentReport);
vscode.window.showInformationMessage(
t('methodReview.complete', { 0: String(totalIssues) })
);
});
})
);
context.subscriptions.push(
vscode.commands.registerCommand('codeReviewer.openPanel', () => {
ReviewPanel.createOrShow(context.extensionUri);
+39 -10
View File
@@ -6,6 +6,7 @@ import js from '@eslint/js';
import ts from 'typescript-eslint';
import type { LinterAdapter, AdapterResult, LinterDiagnostic } from './adapter';
import { getEslintConfigPath } from '../config';
import { t } from '../i18n/messages';
const extraRules: Record<string, 'error' | 'warn'> = {
'eqeqeq': 'error',
@@ -61,17 +62,25 @@ const extraTsRules: Record<string, 'error' | 'warn' | 'off'> = {
const TS_FILES = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'];
const PROJECT_CONFIG_FILES = [
'eslint.config.js',
'eslint.config.mjs',
'eslint.config.cjs',
'eslint.config.ts',
'eslint.config.mts',
'eslint.config.cts',
];
const LEGACY_CONFIG_FILES = [
'.eslintrc.js',
'.eslintrc.cjs',
'.eslintrc.json',
'.eslintrc.yaml',
'.eslintrc.yml',
'.eslintrc',
'eslint.config.js',
'eslint.config.mjs',
];
function findProjectConfig(dir: string): string | null {
for (const name of PROJECT_CONFIG_FILES) {
function findConfigFile(dir: string, names: string[]): string | null {
for (const name of names) {
const p = path.join(dir, name);
if (fs.existsSync(p)) {
return p;
@@ -80,18 +89,30 @@ function findProjectConfig(dir: string): string | null {
return null;
}
function resolveEslintConfig(workingDir: string): { configFile?: string; overrideConfig?: any[] } {
type EslintConfigResult =
| { kind: 'use'; config: { overrideConfigFile?: string | true; overrideConfig?: any[] } }
| { kind: 'legacy'; path: string };
function resolveEslintConfig(workingDir: string): EslintConfigResult {
const globalPath = getEslintConfigPath();
if (globalPath && globalPath.trim() !== '') {
return { configFile: globalPath };
const abs = path.isAbsolute(globalPath) ? globalPath : path.resolve(workingDir, globalPath);
if (fs.existsSync(abs)) {
return { kind: 'use', config: { overrideConfigFile: abs } };
}
}
const projectConfig = findProjectConfig(workingDir);
const projectConfig = findConfigFile(workingDir, PROJECT_CONFIG_FILES);
if (projectConfig) {
return { configFile: projectConfig };
return { kind: 'use', config: { overrideConfigFile: projectConfig } };
}
return { overrideConfig: ESLintAdapter.getDefaultConfig() };
const legacyConfig = findConfigFile(workingDir, LEGACY_CONFIG_FILES);
if (legacyConfig) {
return { kind: 'legacy', path: legacyConfig };
}
return { kind: 'use', config: { overrideConfigFile: true, overrideConfig: ESLintAdapter.getDefaultConfig() } };
}
export class ESLintAdapter implements LinterAdapter {
@@ -119,9 +140,17 @@ export class ESLintAdapter implements LinterAdapter {
async check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult> {
try {
const resolved = resolveEslintConfig(workingDir);
if (resolved.kind === 'legacy') {
return {
diagnostics: [],
status: 'execution-failed',
errorMessage: t('adapter.eslintLegacyConfig', { 0: resolved.path }),
};
}
const engine = new ESLint({
cwd: workingDir,
...resolved,
...resolved.config,
});
const ext = document.languageId === 'typescript' ? 'ts' : 'js';
const isVirtual = document.uri.scheme === 'untitled';
+31 -5
View File
@@ -3,7 +3,7 @@ import type { LinterAdapter, LinterDiagnostic, AdapterResult } from '../types';
import { PmdAdapter } from './pmd';
import { ESLintAdapter } from './eslint';
import { StylelintAdapter } from './stylelint';
import { extractJspSections } from '../jsp/jsp-extractor';
import { extractJspSections, type JspSection } from '../jsp/jsp-extractor';
import { getLinterForLanguage } from '../config';
function mockDocument(code: string, language: string): vscode.TextDocument {
@@ -53,6 +53,28 @@ function mockDocument(code: string, language: string): vscode.TextDocument {
} as unknown as vscode.TextDocument;
}
const WRAP_TEMPLATES: Record<NonNullable<JspSection['scriptletKind']>, {
header: string;
footer: string;
headerLines: number;
}> = {
statement: { header: 'package jsp;\nclass JspScriptlet {\n void run() {\n', footer: '\n }\n}', headerLines: 3 },
expression: { header: 'package jsp;\nclass JspScriptlet {\n Object run() {\n return\n', footer: '\n }\n}', headerLines: 4 },
declaration: { header: 'package jsp;\nclass JspScriptlet {\n', footer: '\n}', headerLines: 2 },
};
function wrapJavaSection(section: JspSection): { code: string; headerLines: number } {
if (section.language !== 'java' || !section.scriptletKind) {
return { code: section.code, headerLines: 0 };
}
const tmpl = WRAP_TEMPLATES[section.scriptletKind];
let body = section.code;
if (section.scriptletKind === 'expression' && body.trim() !== '' && !body.trim().endsWith(';')) {
body += ';';
}
return { code: tmpl.header + body + tmpl.footer, headerLines: tmpl.headerLines };
}
export class JspAdapter implements LinterAdapter {
id = 'jsp';
supportedLanguages = ['jsp', 'html'];
@@ -69,7 +91,7 @@ export class JspAdapter implements LinterAdapter {
const cssEnabled = getLinterForLanguage('css') !== '';
const javaEnabled = getLinterForLanguage('java') !== '';
const pmdResult = await this.pmdAdapter.check(document, workingDir);
const pmdResult = await this.pmdAdapter.checkJsp(document, workingDir);
allDiagnostics.push(...pmdResult.diagnostics);
if (pmdResult.status !== 'ok') {
errors.push(`PMD: ${pmdResult.errorMessage ?? pmdResult.status}`);
@@ -87,13 +109,17 @@ export class JspAdapter implements LinterAdapter {
if (!adapter) { continue; }
try {
const result = await adapter.check(mockDocument(section.code, section.language), workingDir);
const { code, headerLines } = wrapJavaSection(section);
const result = await adapter.check(mockDocument(code, section.language), workingDir);
for (const diag of result.diagnostics) {
const startLine = diag.range.start.line - headerLines;
const endLine = diag.range.end.line - headerLines;
if (startLine < 0) { continue; }
const adjustedRange = new vscode.Range(
diag.range.start.line + section.lineOffset,
startLine + section.lineOffset,
diag.range.start.character,
diag.range.end.line + section.lineOffset,
endLine + section.lineOffset,
diag.range.end.character,
);
allDiagnostics.push({ ...diag, range: adjustedRange });
+29 -12
View File
@@ -3,7 +3,7 @@ import * as path from 'path';
import { existsSync } from 'fs';
import { execSync, spawn } from 'child_process';
import type { LinterAdapter, LinterDiagnostic, AdapterResult } from '../types';
import { getPMDJarPath, getPMDRulesetPath } from '../config';
import { getPMDJarPath, getPMDRulesetPath, getPMDJspRulesetPath } from '../config';
import { t } from '../i18n/messages';
export class PmdAdapter implements LinterAdapter {
@@ -52,23 +52,22 @@ export class PmdAdapter implements LinterAdapter {
}
async check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult> {
return this.run(document, workingDir, false);
}
async checkJsp(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult> {
return this.run(document, workingDir, true);
}
private async run(document: vscode.TextDocument, workingDir: string, isJsp: boolean): Promise<AdapterResult> {
try {
const globalRuleset = getPMDRulesetPath();
let ruleset: string;
if (globalRuleset && globalRuleset.trim() !== '') {
ruleset = globalRuleset;
} else {
const projectRuleset = path.join(workingDir, 'ruleset.xml');
ruleset = existsSync(projectRuleset)
? projectRuleset
: path.join(this.getPmdRunnerClasspath(), 'pmd-java-ruleset.xml');
}
const ruleset = isJsp ? this.resolveJspRuleset() : this.resolveJavaRuleset(workingDir);
const classpath = `${this.getPmdLibClasspath()};${this.getPmdRunnerClasspath()}`;
const isVirtual = document.uri.scheme === 'untitled';
const fileArg = isVirtual ? '-' : document.uri.fsPath;
const javaArgs = ['-cp', classpath, 'PmdRunner', fileArg, ruleset];
const javaArgs = ['-cp', classpath, 'PmdRunner', fileArg, ruleset, isJsp ? 'jsp' : 'java'];
const result = await this.execPmd(javaArgs, isVirtual ? document.getText() : null, workingDir);
const diagnostics = this.parsePmdOutput(result);
@@ -82,6 +81,24 @@ export class PmdAdapter implements LinterAdapter {
}
}
private resolveJavaRuleset(workingDir: string): string {
const globalRuleset = getPMDRulesetPath();
if (globalRuleset && globalRuleset.trim() !== '') {
return globalRuleset;
}
const projectRuleset = path.join(workingDir, 'ruleset.xml');
return existsSync(projectRuleset)
? projectRuleset
: path.join(this.getPmdRunnerClasspath(), 'pmd-java-ruleset.xml');
}
private resolveJspRuleset(): string {
const globalRuleset = getPMDJspRulesetPath();
return globalRuleset && globalRuleset.trim() !== ''
? globalRuleset
: path.join(this.getPmdRunnerClasspath(), 'pmd-jsp-ruleset.xml');
}
private execPmd(args: string[], stdinInput: string | null, cwd: string): Promise<string> {
return new Promise((resolve, reject) => {
const proc = spawn('java', args, { cwd });
@@ -4,32 +4,44 @@ import * as path from 'path';
import * as os from 'os';
import { spawn } from 'child_process';
import type { LinterAdapter, AdapterResult, LinterDiagnostic, Severity } from './adapter';
import { getSqlLintConfigFile } from '../config';
import { getSqlFluffConfigFile, getSqlFluffDialect } from '../config';
import { t } from '../i18n/messages';
import staticRules from '../rules/static-rules.json';
const DIALECT_MAP: Record<string, string> = {
sql: 'ansi',
plsql: 'postgres',
sql: 'mysql',
plsql: 'oracle',
};
const BUILTIN_SQLFLUFF_CONFIG = `[sqlfluff]
rules = core,AM03,AM05,AM08,CV01,CV02,CV06,CV08,CV12,LT13,LT14,LT15,ST01,ST02,ST04,ST05,ST06,ST07,ST09,ST10,ST11,ST12,RF02,RF04,RF05,RF06
dialect = ansi
const SUPPORTED_DIALECTS = [
'ansi', 'athena', 'bigquery', 'clickhouse', 'databricks', 'db2', 'doris',
'duckdb', 'exasol', 'flink', 'greenplum', 'hive', 'impala', 'mariadb',
'materialize', 'mysql', 'oracle', 'postgres', 'redshift', 'snowflake',
'soql', 'sparksql', 'sqlite', 'starrocks', 'teradata', 'trino', 'tsql', 'vertica',
];
const BUILTIN_SQLFLUFF_RULES =
'core,AM03,AM05,AM08,CV01,CV02,CV06,CV08,CV12,LT13,LT14,LT15,ST01,ST02,ST04,ST05,ST06,ST07,ST09,ST10,ST11,ST12,RF02,RF04,RF05,RF06';
function buildBuiltinConfig(dialect: string): string {
return `[sqlfluff]
rules = ${BUILTIN_SQLFLUFF_RULES}
dialect = ${dialect}
max_line_length = 80
indent_unit = space
tab_space_size = 4
`;
}
interface RuleEntry { id: string; description: string; tier?: string; }
const tierMap = new Map<string, string>();
try {
const sqlfluffRules = (staticRules as any).rules?.['sql-lint'] as RuleEntry[] | undefined;
const sqlfluffRules = (staticRules as any).rules?.['sqlfluff'] as RuleEntry[] | undefined;
if (sqlfluffRules) {
for (const rule of sqlfluffRules) {
if (rule.id && rule.tier) {
tierMap.set(rule.id.replace('sql-lint/', ''), rule.tier);
tierMap.set(rule.id.replace('sqlfluff/', ''), rule.tier);
}
}
}
@@ -65,9 +77,12 @@ interface SqlFluffResult {
violations: SqlFluffViolation[];
}
function runSqlfluff(dialect: string, code: string, cwd: string, configPath?: string): Promise<string> {
function runSqlfluff(code: string, cwd: string, configPath?: string, dialect?: string): Promise<string> {
return new Promise((resolve, reject) => {
const args = ['lint', '--dialect', dialect, '--format', 'json'];
const args = ['lint', '--format', 'json'];
if (dialect) {
args.push('--dialect', dialect);
}
if (configPath) {
args.push('--config', configPath);
}
@@ -104,8 +119,8 @@ function runSqlfluff(dialect: string, code: string, cwd: string, configPath?: st
});
}
export class SqlLintAdapter implements LinterAdapter {
id = 'sql-lint';
export class SqlFluffAdapter implements LinterAdapter {
id = 'sqlfluff';
supportedLanguages = ['sql', 'plsql'];
isAvailable(): boolean {
@@ -114,23 +129,28 @@ export class SqlLintAdapter implements LinterAdapter {
async check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult> {
const languageId = document.languageId;
const dialect = DIALECT_MAP[languageId] || 'ansi';
const fallbackDialect = DIALECT_MAP[languageId] ?? 'ansi';
const explicitDialect = getSqlFluffDialect();
const cliDialect = explicitDialect && SUPPORTED_DIALECTS.includes(explicitDialect)
? explicitDialect
: undefined;
let configPath: string | undefined;
let tempConfigPath: string | undefined;
const globalConfig = getSqlLintConfigFile();
const globalConfig = getSqlFluffConfigFile();
if (globalConfig && globalConfig.trim() !== '') {
configPath = globalConfig;
} else if (hasProjectSqlfluffConfig(workingDir)) {
} else {
tempConfigPath = path.join(os.tmpdir(), `vscode-code-reviewer-sqlfluff-${Date.now()}.cfg`);
fs.writeFileSync(tempConfigPath, BUILTIN_SQLFLUFF_CONFIG, 'utf-8');
fs.writeFileSync(tempConfigPath, buildBuiltinConfig(cliDialect ?? fallbackDialect), 'utf-8');
configPath = tempConfigPath;
}
try {
const stdout = await runSqlfluff(dialect, document.getText(), workingDir, configPath);
const stdout = await runSqlfluff(document.getText(), workingDir, configPath, cliDialect);
const results: SqlFluffResult[] = JSON.parse(stdout);
const diagnostics: LinterDiagnostic[] = [];
@@ -138,7 +158,7 @@ export class SqlLintAdapter implements LinterAdapter {
for (const v of result.violations) {
diagnostics.push({
severity: tierToSeverity(tierMap.get(v.code)),
ruleId: `sql-lint:${v.code}`,
ruleId: `sqlfluff:${v.code}`,
message: v.description,
range: new vscode.Range(
v.start_line_no - 1,
+384 -4
View File
@@ -1,5 +1,6 @@
import * as vscode from 'vscode';
import type { AIProvider } from './providers/base';
import { EmptyContentError } from './providers/base';
import type { AIProvider, ChatOptions } from './providers/base';
import { createProvider } from './factory';
import { getAIProvider, getAIModel, getAIBaseUrl, getAITemperature, getAITimeout, getAIMaxTokens, getAIOutputLanguage, getApiKey } from '../config';
import type { LinterDiagnostic, CustomRule } from '../types';
@@ -8,7 +9,10 @@ import type {
CustomRuleResult,
TranslatedDiagnostic,
AIFinding,
MethodFinding,
MethodReviewResult,
} from './schema';
import type { MethodScope } from '../scope/method-extractor';
import { t, getLanguage } from '../i18n/messages';
function buildCustomRulePrompt(rules: CustomRule[]): string {
@@ -56,8 +60,11 @@ function repairJsonEscapes(str: string): string {
return out;
}
function parseJsonResponse(raw: string): object {
export function parseJsonResponse(raw: string): object {
const trimmed = raw.trim();
if (trimmed === '') {
throw new Error(t('engine.emptyResponse'));
}
const start = trimmed.indexOf('{');
const end = trimmed.lastIndexOf('}');
if (start === -1 || end === -1) {
@@ -76,6 +83,22 @@ function parseJsonResponse(raw: string): object {
}
}
export async function chatWithRetry(
provider: AIProvider,
systemPrompt: string,
userPrompt: string,
options: ChatOptions
): Promise<string> {
try {
return await provider.chat(systemPrompt, userPrompt, options);
} catch (err) {
if (err instanceof EmptyContentError) {
return provider.chat(systemPrompt, userPrompt, options);
}
throw err;
}
}
function buildCustomRuleSystemPrompt(): string {
const lang = getLanguage();
if (lang === 'ja') {
@@ -214,14 +237,16 @@ export async function runAIReview(
const requestA =
customRules.length > 0
? provider.chat(
? chatWithRetry(
provider,
buildCustomRuleSystemPrompt(),
buildUserPromptCustomRules(customRules, numberedCode),
options
)
: Promise.resolve('{}');
const requestB = provider.chat(
const requestB = chatWithRetry(
provider,
buildDeepReviewSystemPrompt(),
buildUserPromptDeepReview(numberedCode, staticDiagnostics),
options
@@ -272,3 +297,358 @@ export async function runAIReview(
error: errors.join('; '),
};
}
export async function runMethodReview(
context: vscode.ExtensionContext,
scope: MethodScope,
customRules: CustomRule[]
): Promise<MethodReviewResult> {
const apiKey = await getApiKey(context);
if (!apiKey) {
return {
customRuleResults: [],
findings: [],
degraded: true,
error: t('adapter.noApiKey'),
};
}
const providerId = getAIProvider();
const baseUrl = getAIBaseUrl();
let provider: AIProvider;
try {
provider = createProvider(providerId, apiKey, baseUrl, context.extensionUri);
} catch (err) {
return {
customRuleResults: [],
findings: [],
degraded: true,
error: t('adapter.createProviderFail', { 0: err instanceof Error ? err.message : String(err) }),
};
}
const options = {
model: getAIModel(),
temperature: getAITemperature(),
maxTokens: getAIMaxTokens(),
timeoutMs: getAITimeout() * 1000,
};
const numberedCode = addLineNumbers(scope.code);
const hasRules = customRules.length > 0;
let response: string;
try {
response = await chatWithRetry(
provider,
buildMethodReviewSystemPrompt(hasRules),
buildMethodUserPrompt(scope, numberedCode, customRules),
options
);
} catch (err) {
return {
customRuleResults: [],
findings: [],
degraded: true,
error: t('adapter.aiReviewRequestFail', { 0: err instanceof Error ? err.message : String(err) }),
};
}
const errors: string[] = [];
let customRuleResults: CustomRuleResult[] = [];
let findings: MethodFinding[] = [];
try {
const parsed = parseJsonResponse(response) as {
customRuleResults?: CustomRuleResult[];
findings?: MethodFinding[];
};
customRuleResults = (parsed.customRuleResults ?? []).map(r => {
const id = String(r.ruleId ?? '');
return { ...r, ruleId: id.startsWith('custom:') ? id : `custom:${id}` };
});
findings = (parsed.findings ?? []).map(f => {
const id = String(f.ruleId ?? '');
return { ...f, ruleId: id.startsWith('method:') ? id : `method:${id}` };
});
} catch (e) {
errors.push(t('adapter.aiReviewParseFail', { 0: e instanceof Error ? e.message : String(e) }));
}
return {
customRuleResults,
findings,
degraded: errors.length > 0,
error: errors.join('; ') || undefined,
};
}
function buildMethodReviewSystemPrompt(hasRules: boolean): string {
const lang = getLanguage();
if (lang === 'ja') {
return buildMethodSystemPromptJa(hasRules);
}
if (lang === 'en') {
return buildMethodSystemPromptEn(hasRules);
}
return buildMethodSystemPromptZh(hasRules);
}
function buildMethodSystemPromptEn(hasRules: boolean): string {
const ruleSection = hasRules
? `## Task 1: Custom Rule Matching
Evaluate whether the method violates any of the provided custom rules.
Understand semantics, not text matching.
Report violations in "customRuleResults".\n\n`
: '';
const ruleOutput = hasRules
? ` "customRuleResults": [
{
"ruleId": "original rule id",
"line": line_number,
"severity": "error|warning|info",
"message": "violation description"
}
],\n`
: '';
return `You are a senior code review expert reviewing a single method.
There is no static analysis before you — you handle rule matching AND deep review.
${ruleSection}## Review Strategy: Path Enumeration
- Walk through every if/else/switch branch, note coverage and gaps
- Enumerate boundary values for every parameter (null, empty collection, extreme values, wrong types)
- Check every throw/catch path for proper fallback strategy
- Trace the method's role in its call chain
## Required Dimensions (do not skip any)
A. Correctness: branch coverage, boundary conditions, exception path completeness
B. Security: input validation, injection risk, permission check, sensitive data leakage
C. Design: single responsibility, parameter design, return value contract, call chain adaptation
D. Convention: naming, cyclomatic complexity, magic numbers, missing comments
E. Performance: time/space complexity, resource leaks, unnecessary computation
F. Testability: side effect isolation, dependency mockability, deterministic output
## Call Chain Analysis
- Check whether callers' arguments match this method's expectations
- Check whether this method's return value is correctly handled by callers
- Check whether exceptions are caught or declared by callers
Output JSON only. Double quotes in strings must be escaped with \\".
Format:
{
${ruleOutput} "findings": [
{
"ruleId": "method-boundary-null",
"severity": "error|warning|info",
"category": "correctness|security|design|convention|performance|testability",
"title": "issue title",
"description": "detailed description",
"suggestion": "fix suggestion",
"codeDiff": "optional fix diff",
"line": line_number,
"path": "trigger path description, e.g. if(order==null) -> NPE on .getId()"
}
]
}
If no issues found, return empty arrays.
Output language: en`;
}
function buildMethodSystemPromptZh(hasRules: boolean): string {
const ruleSection = hasRules
? `## 任务一:自定义规则匹配
评估方法是否违反了提供的自定义规则。
理解语义,而非文本匹配。
在 "customRuleResults" 中报告违规。\n\n`
: '';
const ruleOutput = hasRules
? ` "customRuleResults": [
{
"ruleId": "原始规则 ID",
"line": 行号,
"severity": "error|warning|info",
"message": "违规描述"
}
],\n`
: '';
return `你是资深代码审查专家,正在审查单个方法。
没有静态分析的前置过滤——你同时负责规则匹配和深度审查。
${ruleSection}## 审查策略:逐路径枚举
- 遍历每个 if/else/switch 分支,标注覆盖与遗漏
- 枚举每个入参的边界值(null、空集合、极值、错误类型)
- 检查每个 throw/catch 路径的降级策略
- 追踪方法在调用链中的角色
## 必须覆盖的维度(不可跳过)
A. 正确性:分支覆盖、边界条件、异常路径完整性
B. 安全性:输入校验、注入风险、权限检查、敏感信息泄露
C. 设计:职责单一性、参数设计合理性、返回值契约、调用链适配
D. 规范:命名、圈复杂度、魔法数字、注释缺失
E. 性能:时间/空间复杂度、资源泄漏、不必要的计算
F. 可测试性:副作用隔离、依赖可 Mock 性、确定性输出
## 调用链分析
- 检查调用者传入的参数是否符合本方法预期
- 检查本方法的返回值是否被调用者正确处理
- 检查异常是否被调用者捕获或声明
输出 JSON,字符串中的双引号必须用 \\" 转义。
格式:
{
${ruleOutput} "findings": [
{
"ruleId": "method-boundary-null",
"severity": "error|warning|info",
"category": "correctness|security|design|convention|performance|testability",
"title": "问题标题",
"description": "详细描述",
"suggestion": "修复建议",
"codeDiff": "可选的修复 diff",
"line": 行号,
"path": "触发路径描述,如 if(order==null) -> NPE on .getId()"
}
]
}
如果未发现问题,返回空数组。
输出语言:zh-CN`;
}
function buildMethodSystemPromptJa(hasRules: boolean): string {
const ruleSection = hasRules
? `## タスク1:カスタムルールマッチング
提供されたカスタムルールの違反があるか評価してください。
意味を理解し、テキストの一致ではなく判断してください。
違反を "customRuleResults" で報告してください。\n\n`
: '';
const ruleOutput = hasRules
? ` "customRuleResults": [
{
"ruleId": "元のルールID",
"line": 行番号,
"severity": "error|warning|info",
"message": "違反の説明"
}
],\n`
: '';
return `あなたはシニアコードレビュー専門家です。単一のメソッドをレビューしています。
事前の静的解析はありません——あなたがルールマッチングと詳細レビューの両方を担当します。
${ruleSection}## レビュー戦略:パス列挙
- すべての if/else/switch 分岐を辿り、カバレッジと漏れを確認
- すべての引数の境界値(null、空コレクション、極値、誤った型)を列挙
- すべての throw/catch パスのフォールバック戦略を確認
- コールチェーンにおけるメソッドの役割を追跡
## 必須カバレッジ(スキップ不可)
A. 正しさ:分岐カバレッジ、境界条件、例外パスの完全性
B. セキュリティ:入力検証、インジェクションリスク、権限チェック、機密情報漏洩
C. 設計:単一責任、パラメータ設計、戻り値契約、コールチェーン適合
D. 規約:命名、循環的複雑度、マジックナンバー、コメント欠落
E. パフォーマンス:時間/空間複雑度、リソースリーク、不要な計算
F. テスタビリティ:副作用の分離、依存のモック化容易性、決定的出力
## コールチェーン分析
- 呼び出し元の引数がこのメソッドの期待と一致しているか確認
- このメソッドの戻り値が呼び出し元で正しく処理されているか確認
- 例外が呼び出し元でキャッチまたは宣言されているか確認
JSONのみを出力。文字列内の二重引用符は \\" でエスケープしてください。
形式:
{
${ruleOutput} "findings": [
{
"ruleId": "method-boundary-null",
"severity": "error|warning|info",
"category": "correctness|security|design|convention|performance|testability",
"title": "問題のタイトル",
"description": "詳細な説明",
"suggestion": "修正提案",
"codeDiff": "オプションの修正diff",
"line": 行番号,
"path": "トリガーパス説明、例: if(order==null) -> .getId() で NPE"
}
]
}
問題がない場合は空配列を返してください。
出力言語:ja`;
}
interface MethodPromptLabels {
signature: string;
code: string;
rule: string;
chain: string;
role: string;
callers: string;
callees: string;
none: string;
}
function getMethodPromptLabels(lang: string): MethodPromptLabels {
if (lang === 'ja') {
return {
signature: 'メソッド署名',
code: 'メソッドコード(行番号付き)',
rule: 'マッチングするカスタムルール',
chain: 'コールチェーンコンテキスト',
role: '業務フローでの役割',
callers: '呼び出し元',
callees: '呼び出し先',
none: '(なし)',
};
}
if (lang === 'en') {
return {
signature: 'Method Signature',
code: 'Method Code (with line numbers)',
rule: 'Custom Rules to Match',
chain: 'Call Chain Context',
role: 'Role in Business Flow',
callers: 'Callers',
callees: 'Callees',
none: '(none)',
};
}
return {
signature: '方法签名',
code: '方法代码(带行号)',
rule: '需匹配的自定义规则',
chain: '调用链上下文',
role: '业务流中的角色',
callers: '调用者',
callees: '被调用者',
none: '(无)',
};
}
function buildMethodUserPrompt(
scope: MethodScope,
numberedCode: string,
customRules: CustomRule[]
): string {
const labels = getMethodPromptLabels(getLanguage());
let ruleBlock = '';
if (customRules.length > 0) {
const ruleLines = customRules
.map((r, i) => `${i + 1}. [${r.id}] (${r.severity}) ${r.description}\n ${r.message}`)
.join('\n');
ruleBlock = `\n## ${labels.rule}\n${ruleLines}\n`;
}
return `## ${labels.signature}
${scope.signature}
## ${labels.code}
${numberedCode}
${ruleBlock}
## ${labels.chain}
${labels.role}: ${scope.role}
${labels.callers}: ${scope.callers.length > 0 ? scope.callers.join(', ') : labels.none}
${labels.callees}: ${scope.callees.length > 0 ? scope.callees.join(', ') : labels.none}`;
}
+7
View File
@@ -21,3 +21,10 @@ export abstract class AIProvider {
options: ChatOptions
): Promise<string>;
}
export class EmptyContentError extends Error {
constructor(detail: string) {
super(detail);
this.name = 'EmptyContentError';
}
}
+15 -2
View File
@@ -1,4 +1,5 @@
import { AIProvider, ChatOptions } from './base';
import { AIProvider, ChatOptions, EmptyContentError } from './base';
import { t } from '../../i18n/messages';
export class ClaudeProvider extends AIProvider {
id = 'claude';
@@ -42,8 +43,20 @@ export class ClaudeProvider extends AIProvider {
const data = await response.json() as {
content?: Array<{ text?: string }>;
stop_reason?: string;
error?: { message?: string };
};
return data.content?.[0]?.text ?? '';
const text = data.content?.[0]?.text;
if (text === undefined || text === null || text.trim() === '') {
const parts = [`stop_reason=${data.stop_reason ?? 'unknown'}`];
if (data.error?.message) {
parts.push(data.error.message);
}
throw new EmptyContentError(t('adapter.emptyContent', { 0: parts.join(', ') }));
}
return text;
} finally {
clearTimeout(timeout);
}
+17 -2
View File
@@ -1,4 +1,5 @@
import { AIProvider, ChatOptions } from './base';
import { AIProvider, ChatOptions, EmptyContentError } from './base';
import { t } from '../../i18n/messages';
export class GeminiProvider extends AIProvider {
id = 'gemini';
@@ -42,9 +43,23 @@ export class GeminiProvider extends AIProvider {
candidates?: Array<{
content?: { parts?: Array<{ text?: string }> };
}>;
promptFeedback?: { blockReason?: string };
error?: { message?: string };
};
return data.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
if (text === undefined || text === null || text.trim() === '') {
const parts = [
`candidates=${data.candidates?.length ?? 0}`,
`blockReason=${data.promptFeedback?.blockReason ?? 'none'}`,
];
if (data.error?.message) {
parts.push(data.error.message);
}
throw new EmptyContentError(t('adapter.emptyContent', { 0: parts.join(', ') }));
}
return text;
} finally {
clearTimeout(timeout);
}
+24 -3
View File
@@ -1,4 +1,4 @@
import { AIProvider, ChatOptions } from './base';
import { AIProvider, ChatOptions, EmptyContentError } from './base';
import { t } from '../../i18n/messages';
export class OpenAICompatibleProvider extends AIProvider {
@@ -53,9 +53,30 @@ export class OpenAICompatibleProvider extends AIProvider {
}
const data = await response.json() as {
choices: Array<{ message: { content: string } }>;
choices?: Array<{
message?: { content?: string | null };
finish_reason?: string | null;
}>;
error?: { message?: string };
};
return data.choices[0]?.message?.content ?? '';
const content = data.choices?.[0]?.message?.content;
if (content === undefined || content === null || content.trim() === '') {
const finish = data.choices?.[0]?.finish_reason ?? 'unknown';
if (finish === 'length') {
throw new EmptyContentError(t('adapter.maxTokensTruncated', { 0: String(options.maxTokens) }));
}
const parts = [
`finish_reason=${finish}`,
`choices=${data.choices?.length ?? 0}`,
];
if (data.error?.message) {
parts.push(data.error.message);
}
throw new EmptyContentError(t('adapter.emptyContent', { 0: parts.join(', ') }));
}
return content;
} finally {
clearTimeout(timeout);
}
+21 -1
View File
@@ -15,7 +15,7 @@ export interface CustomRuleResult {
export interface AIFinding {
ruleId: string;
severity: 'error' | 'warning' | 'info';
category: 'bug' | 'performance' | 'security' | 'style' | 'design';
category: 'bug' | 'performance' | 'security' | 'style' | 'design' | 'correctness' | 'convention' | 'testability';
title: string;
description: string;
suggestion: string;
@@ -23,6 +23,26 @@ export interface AIFinding {
line: number;
}
export type MethodFindingCategory =
| 'correctness'
| 'security'
| 'design'
| 'convention'
| 'performance'
| 'testability';
export interface MethodFinding extends AIFinding {
category: MethodFindingCategory;
path?: string;
}
export interface MethodReviewResult {
customRuleResults: CustomRuleResult[];
findings: MethodFinding[];
degraded: boolean;
error?: string;
}
export interface AIResponse {
translatedDiagnostics: TranslatedDiagnostic[];
customRuleResults: CustomRuleResult[];
+6 -2
View File
@@ -18,8 +18,12 @@ export function getPMDJspRulesetPath(): string {
return vscode.workspace.getConfiguration(ROOT).get<string>('pmd.jspRulesetPath', '');
}
export function getSqlLintConfigFile(): string {
return vscode.workspace.getConfiguration(ROOT).get<string>('sql-lint.configFile', '');
export function getSqlFluffConfigFile(): string {
return vscode.workspace.getConfiguration(ROOT).get<string>('sqlfluff.configFile', '');
}
export function getSqlFluffDialect(): string {
return vscode.workspace.getConfiguration(ROOT).get<string>('sqlfluff.dialect', '');
}
export function getEslintConfigPath(): string {
+19 -1
View File
@@ -4,6 +4,8 @@ import { registerCommands } from './activation/commands';
import { SetupViewProvider } from './views/setupView';
import { setLanguage, t, type Language } from './i18n/messages';
import { getAIOutputLanguage } from './config';
import { ReviewStatusCache } from './scope/status-cache';
import { MethodCodeLensProvider } from './views/codeLensProvider';
let orchestrator: Orchestrator;
@@ -19,7 +21,23 @@ export function activate(context: vscode.ExtensionContext) {
vscode.window.registerWebviewViewProvider('codeReviewer.setupView', setupProvider)
);
registerCommands(context, orchestrator);
const statusCache = new ReviewStatusCache();
const codeLensProvider = new MethodCodeLensProvider(statusCache);
context.subscriptions.push(
vscode.languages.registerCodeLensProvider(
{ scheme: 'file' },
codeLensProvider
)
);
context.subscriptions.push(
vscode.workspace.onDidCloseTextDocument((document) => {
statusCache.clearDocument(document.uri);
})
);
registerCommands(context, orchestrator, codeLensProvider, statusCache);
const debounceTimers = new Map<string, NodeJS.Timeout>();
+132 -6
View File
@@ -142,6 +142,11 @@ const messages: Record<string, Record<Language, string>> = {
en: '✗ Connection failed: {0}',
ja: '✗ 接続失敗: {0}',
},
'setup.emptyResponse': {
'zh-CN': 'AI 返回空响应,请检查模型配置',
en: 'AI returned an empty response, please check the model configuration',
ja: 'AIが空の応答を返しました。モデル設定を確認してください',
},
'setup.selectRuleFile': {
'zh-CN': '选择规则文件',
en: 'Select rule file',
@@ -523,9 +528,9 @@ const messages: Record<string, Record<Language, string>> = {
ja: 'JS, TS, JSX, TSXJSP内のJavaScriptコードを含む、例: <script>',
},
'setup.adapter.eslintGuide': {
'zh-CN': '项目根目录创建 .eslintrc.js 或在 VS Code 设置中配置 eslintConfigPath',
en: 'Create .eslintrc.js in project root or set eslintConfigPath in VS Code settings',
ja: 'プロジェクトルートに.eslintrc.jsを作成するか、VS Code設定でeslintConfigPathを設定してください',
'zh-CN': '项目根目录创建 eslint.config.js 或在 VS Code 设置中配置 eslintConfigPath',
en: 'Create eslint.config.js in project root or set eslintConfigPath in VS Code settings',
ja: 'プロジェクトルートにeslint.config.jsを作成するか、VS Code設定でeslintConfigPathを設定してください',
},
'setup.adapter.stylelintLanguages': {
'zh-CN': 'CSS, SCSS, Less(含 JSP 中的 CSS 代码,例如<style>',
@@ -602,6 +607,36 @@ const messages: Record<string, Record<Language, string>> = {
en: 'Overlap reason: {0}',
ja: '重複理由:{0}',
},
'import.dupExactTitle': {
'zh-CN': '完全重复',
en: 'Exact duplicate',
ja: '完全重複',
},
'import.dupOverlapTitle': {
'zh-CN': '部分重叠',
en: 'Partial overlap',
ja: '部分的重複',
},
'import.dupExactText': {
'zh-CN': '与规则「{0}」完全重复',
en: 'Exact duplicate of rule "{0}"',
ja: 'ルール「{0}」と完全重複',
},
'import.dupOverlapText': {
'zh-CN': '与规则「{0}」部分重叠',
en: 'Partially overlaps rule "{0}"',
ja: 'ルール「{0}」と部分的に重複',
},
'import.dupDescriptionLabel': {
'zh-CN': 'description{0}',
en: 'description: {0}',
ja: 'description{0}',
},
'import.dupExactHint': {
'zh-CN': '默认将注释导入,点击「保留」可恢复',
en: 'Imported as commented out by default; click "Keep" to restore',
ja: 'デフォルトでコメントアウトとしてインポートされます。「保持」をクリックすると復元します',
},
'import.badgeRestored': {
'zh-CN': '已恢复',
en: 'Restored',
@@ -712,6 +747,21 @@ const messages: Record<string, Record<Language, string>> = {
en: 'Rule id cannot be empty',
ja: 'ルールIDは必須です',
},
'import.idMissing': {
'zh-CN': 'id 缺失,请补充',
en: 'id missing, please fill in',
ja: 'id がありません、入力してください',
},
'import.severityMissing': {
'zh-CN': 'severity 缺失',
en: 'severity missing',
ja: 'severity が未設定',
},
'import.severitySelectHint': {
'zh-CN': '请选择 severity',
en: 'Select severity',
ja: 'severity を選択',
},
'report.panelTitle': {
'zh-CN': '净码特工 · 代码审查报告',
@@ -1002,6 +1052,11 @@ const messages: Record<string, Record<Language, string>> = {
en: 'JSON parse failed. Raw response (first 200 chars): {0}',
ja: 'JSON解析に失敗しました。生のレスポンス(先頭200文字):{0}',
},
'engine.emptyResponse': {
'zh-CN': 'AI 返回空响应',
en: 'AI returned an empty response',
ja: 'AIが空の応答を返しました',
},
'adapter.javaNotInstalled': {
'zh-CN': 'Java 11+ 未安装或不在 PATH 中',
@@ -1048,6 +1103,21 @@ const messages: Record<string, Record<Language, string>> = {
en: 'AI review request failed: {0}',
ja: 'AIレビューリクエストに失敗しました: {0}',
},
'adapter.eslintLegacyConfig': {
'zh-CN': '项目使用旧版 .eslintrc 配置({0}),ESLint v9 已不支持。请迁移到 eslint.config.js 或删除该文件以使用内置规则',
en: 'Project uses legacy .eslintrc config ({0}), which is not supported by ESLint v9. Please migrate to eslint.config.js or remove the file to use built-in rules',
ja: 'プロジェクトは旧形式の.eslintrc設定({0})を使用しています。ESLint v9ではサポートされていません。eslint.config.jsへの移行、またはファイル削除で組み込みルールを使用してください',
},
'adapter.emptyContent': {
'zh-CN': 'AI 返回空内容({0}',
en: 'AI returned empty content ({0})',
ja: 'AIが空のコンテンツを返しました({0})',
},
'adapter.maxTokensTruncated': {
'zh-CN': 'AI 输出被 max_tokens 截断(当前 {0}),请调大设置 ai.maxTokens 或更换模型',
en: 'AI output was truncated by max_tokens (current: {0}). Please increase ai.maxTokens or switch to a different model',
ja: 'AI出力がmax_tokensで打ち切られました(現在 {0})。ai.maxTokensを増やすか、モデルを変更してください',
},
'extension.activated': {
'zh-CN': '净码特工 · Code Purifier 已激活',
@@ -1116,9 +1186,9 @@ const messages: Record<string, Record<Language, string>> = {
ja: 'エラー行',
},
'import.cannotImport': {
'zh-CN': '将自动丢弃,请修改文件后重新导入',
en: 'Will be auto-discarded, please fix the file and retry',
ja: '自動破棄されます、ファイルを修正して再インポートしてください',
'zh-CN': '需修复后点击添加',
en: 'Fix then click Add',
ja: '修正して「追加」をクリック',
},
'import.dedupFailed': {
'zh-CN': 'AI 去重失败,规则将不带去重标记导入',
@@ -1135,6 +1205,31 @@ const messages: Record<string, Record<Language, string>> = {
en: '⚠',
ja: '⚠',
},
'import.add': {
'zh-CN': '添加',
en: 'Add',
ja: '追加',
},
'import.adding': {
'zh-CN': '校验并去重中...',
en: 'Validating & deduping...',
ja: '検証・重複排除中...',
},
'import.idConflict': {
'zh-CN': 'id {0} 与已有规则重复,请修改 id',
en: 'id {0} conflicts with an existing rule, change the id',
ja: 'id {0} が既存ルールと重複、id を変更してください',
},
'import.addDedupFallback': {
'zh-CN': 'AI 去重失败,已以无重复方式添加',
en: 'AI dedup failed, added as no-duplicate',
ja: 'AI 重複排除失敗、重複なしとして追加',
},
'import.validationSeverityInvalid': {
'zh-CN': 'severity 非法',
en: 'Invalid severity',
ja: 'severity が不正です',
},
'exportTemplate.saveLabel': {
'zh-CN': '导出模板',
en: 'Export Template',
@@ -1155,6 +1250,37 @@ const messages: Record<string, Record<Language, string>> = {
en: 'Reveal in Folder',
ja: 'フォルダを開く',
},
'codelens.reviewMethod': {
'zh-CN': '🔍 Code Purifier: 审查此方法',
en: '🔍 Code Purifier: Review This Method',
ja: '🔍 Code Purifier: このメソッドを審査',
},
'codelens.reviewedClean': {
'zh-CN': '✓ Code Purifier: 已审查(无问题)',
en: '✓ Code Purifier: Reviewed (No Issues)',
ja: '✓ Code Purifier: 審査済み(問題なし)',
},
'codelens.reviewedWithIssues': {
'zh-CN': '✓ Code Purifier: 已审查({0} 个问题)',
en: '✓ Code Purifier: Reviewed ({0} Issues)',
ja: '✓ Code Purifier: 審査済み({0} 件の問題)',
},
'methodReview.running': {
'zh-CN': 'Code Purifier 正在审查方法:{0}',
en: 'Code Purifier: Reviewing method: {0}',
ja: 'Code Purifier がメソッドを審査中:{0}',
},
'methodReview.noMethod': {
'zh-CN': '当前位置未检测到方法',
en: 'No method detected at current position',
ja: '現在位置でメソッドが検出されませんでした',
},
'methodReview.complete': {
'zh-CN': '方法审查完成,发现 {0} 个问题',
en: 'Method review complete, {0} issues found',
ja: 'メソッド審査完了、{0} 件の問題を発見',
},
};
let currentLang: Language = defaultLang;
+24 -3
View File
@@ -4,6 +4,7 @@ export interface JspSection {
lineOffset: number;
sourceStart: number;
sourceEnd: number;
scriptletKind?: 'statement' | 'declaration' | 'expression';
}
export function extractJspSections(content: string): JspSection[] {
@@ -38,17 +39,37 @@ export function extractJspSections(content: string): JspSection[] {
});
}
const scriptletRegex = /<%=?([\s\S]*?)%>/g;
while ((match = scriptletRegex.exec(content)) !== null) {
const code = match[1];
const jspTagRegex = /<%--([\s\S]*?)--%>|<%!([\s\S]*?)%>|<%=([\s\S]*?)%>|<%@([\s\S]*?)%>|<%([\s\S]*?)%>/g;
while ((match = jspTagRegex.exec(content)) !== null) {
const beforeMatch = content.substring(0, match.index);
const lineOffset = beforeMatch.split('\n').length - 1;
let code: string | undefined;
let scriptletKind: JspSection['scriptletKind'] | undefined;
if (match[1] !== undefined || match[4] !== undefined) {
continue;
}
if (match[2] !== undefined) {
code = match[2];
scriptletKind = 'declaration';
} else if (match[3] !== undefined) {
code = match[3];
scriptletKind = 'expression';
} else if (match[5] !== undefined) {
code = match[5];
scriptletKind = 'statement';
}
if (code === undefined) { continue; }
sections.push({
language: 'java',
code,
lineOffset,
sourceStart: match.index,
sourceEnd: match.index + match[0].length,
scriptletKind,
});
}
+35 -17
View File
@@ -45,27 +45,45 @@ interface MergeInput {
};
}
export function mergeResults(input: MergeInput): MergedReport {
const customRuleDiagnostics: LinterDiagnostic[] = input.customRuleResults.map(r => ({
severity: r.severity as Severity,
ruleId: r.ruleId,
message: r.message,
range: new vscode.Range(Math.max(0, r.line - 1), 0, Math.max(0, r.line - 1), 1),
}));
const SEVERITY_RANK: Record<string, number> = { error: 0, warning: 1, info: 2 };
const linterDiagnostics = input.staticDiagnostics.map((d, i) => {
const td = input.translatedDiagnostics[i];
if (td) {
return { ...d, message: td.translatedMessage, suggestion: td.translatedSuggestion || d.suggestion };
}
return d;
function sortBySeverityAndLine<T extends { severity: string }>(items: T[], lineOf: (item: T) => number): T[] {
return [...items].sort((a, b) => {
const rankDiff = (SEVERITY_RANK[a.severity] ?? 3) - (SEVERITY_RANK[b.severity] ?? 3);
if (rankDiff !== 0) { return rankDiff; }
return lineOf(a) - lineOf(b);
});
}
const linterCount = input.staticDiagnostics.length;
export function mergeResults(input: MergeInput): MergedReport {
const customRuleDiagnostics: LinterDiagnostic[] = sortBySeverityAndLine(
input.customRuleResults.map(r => ({
severity: r.severity as Severity,
ruleId: r.ruleId,
message: r.message,
range: new vscode.Range(Math.max(0, r.line - 1), 0, Math.max(0, r.line - 1), 1),
})),
d => d.range.start.line
);
const linterDiagnostics = sortBySeverityAndLine(
input.staticDiagnostics.map((d, i) => {
const td = input.translatedDiagnostics[i];
if (td) {
return { ...d, message: td.translatedMessage, suggestion: td.translatedSuggestion || d.suggestion };
}
return d;
}),
d => d.range.start.line
);
const aiFindings = sortBySeverityAndLine(input.aiFindings, f => f.line);
const linterCount = linterDiagnostics.length;
const customRuleCount = customRuleDiagnostics.length;
const aiCount = input.aiFindings.length;
const aiCount = aiFindings.length;
const fixableLinterIndices = input.staticDiagnostics.map((_, i) => i);
const fixableLinterIndices = linterDiagnostics.map((_, i) => i);
const fixableCustomIndices = customRuleDiagnostics
.map((_, i) => i);
@@ -74,7 +92,7 @@ export function mergeResults(input: MergeInput): MergedReport {
linterDiagnostics,
customRuleDiagnostics,
translatedDiagnostics: input.translatedDiagnostics,
aiFindings: input.aiFindings,
aiFindings,
linterCount,
customRuleCount,
aiCount,
+2 -2
View File
@@ -4,7 +4,7 @@ import { getLinterForLanguage, isAdapterEnabled } from '../config';
import { ESLintAdapter } from '../adapters/eslint';
import { PmdAdapter } from '../adapters/pmd';
import { StylelintAdapter } from '../adapters/stylelint';
import { SqlLintAdapter } from '../adapters/sql-lint';
import { SqlFluffAdapter } from '../adapters/sqlfluff';
import { JspAdapter } from '../adapters/jsp';
export interface StaticAnalysisResult {
@@ -22,7 +22,7 @@ export class Orchestrator {
new ESLintAdapter(),
new PmdAdapter(),
new StylelintAdapter(),
new SqlLintAdapter(),
new SqlFluffAdapter(),
new JspAdapter(),
];
}
+4
View File
@@ -332,6 +332,10 @@ ${errorBox}
const parts: string[] = [`<div class="section-header"><span class="section-header-title">${t('report.sourceAI')} · ${t('report.itemsCount', { 0: report.aiCount })}</span><button class="btn" onclick="send('fixAll')">${t('report.fixAll')}</button></div>`];
for (const f of report.aiFindings) {
const details: string[] = [];
const path = (f as { path?: string }).path;
if (path) {
details.push(`<div class="detail-text">🔗 ${esc(path)}</div>`);
}
details.push(`<div class="detail-text">${esc(f.description)}</div>`);
if (f.category) {
details.push(`<span class="detail-category">🎯 ${esc(f.category)}</span>`);
+5 -19
View File
@@ -1,5 +1,6 @@
import type { CustomRule } from '../../types';
import { getLanguage, type Language } from '../../i18n/messages';
import { buildKnownRulesSection } from './known-rules';
export function buildDedupOnlyPrompt(
yamlContent: string,
@@ -8,12 +9,6 @@ export function buildDedupOnlyPrompt(
const lang = getLanguage();
const s = PROMPTS[lang];
const existingList = existingRules.length === 0
? s.noExisting
: existingRules.map(r =>
`- id: ${r.id} | severity: ${r.severity} | description: ${r.description} | message: ${r.message}`
).join('\n');
const system = [
s.role,
s.taskTitle,
@@ -22,8 +17,7 @@ export function buildDedupOnlyPrompt(
s.rulesLines.join('\n'),
s.constraintTitle,
s.constraintLines.join('\n'),
s.existingTitle,
existingList,
buildKnownRulesSection(existingRules),
].join('\n\n');
const user = s.userPrefix + '\n\n' + yamlContent;
@@ -38,15 +32,13 @@ const PROMPTS: Record<Language, {
rulesLines: string[];
constraintTitle: string;
constraintLines: string[];
existingTitle: string;
noExisting: string;
userPrefix: string;
}> = {
'zh-CN': {
role: '你是规则去重判定助手。你只输出 YAML,不输出任何解释。',
taskTitle: '## 任务',
taskLines: [
'下面是已标准化的规则 YAML。你只负责对照"现有规则"为每条规则标注去重字段。',
'下面是已标准化的规则 YAML。你只负责对照"内置静态分析规则与已导入的自定义规则"为每条规则标注去重字段。',
'为每条规则补充以下字段(如果无重复则标注 none):',
'- duplicateOf: 重复的规则 ID(如 eslint/no-console、custom/my-rule',
'- duplicateLevel: exact(完全相同)/ overlap(部分重叠)/ none(无重复)',
@@ -67,15 +59,13 @@ const PROMPTS: Record<Language, {
'5. 如果某条规则与现有规则无任何重复,设置 duplicateLevel: none 即可,不需要补充 duplicateOf',
'6. 输出纯 YAML,不要用 markdown 代码块包裹',
],
existingTitle: '## 现有规则',
noExisting: '(无)',
userPrefix: '## 待去重的规则 YAML',
},
'en': {
role: 'You are a rule deduplication assistant. Output YAML only, no explanations.',
taskTitle: '## Task',
taskLines: [
'Below is standardized rule YAML. Only annotate dedup fields against the "Existing Rules" list.',
'Below is standardized rule YAML. Only annotate dedup fields by comparing against the "built-in static analysis rules and existing custom rules".',
'For each rule, add (mark none if no conflict):',
'- duplicateOf: duplicated rule ID (e.g. eslint/no-console, custom/my-rule)',
'- duplicateLevel: exact / overlap / none',
@@ -96,15 +86,13 @@ const PROMPTS: Record<Language, {
'5. If a rule has no duplication, set duplicateLevel: none without duplicateOf',
'6. Output pure YAML, do NOT wrap in markdown code fences',
],
existingTitle: '## Existing Rules',
noExisting: '(none)',
userPrefix: '## YAML to deduplicate',
},
'ja': {
role: 'あなたはルール重複判定アシスタントです。YAML のみ出力し、説明は不要です。',
taskTitle: '## タスク',
taskLines: [
'以下は標準化されたルール YAML です。既存ルール照合し、重複フィールドのみ注釈してください。',
'以下は標準化されたルール YAML です。組み込みの静的解析ルールと既存カスタムルール照合し、重複フィールドのみ注釈してください。',
'各ルールに以下を追加(重複がない場合は none と表記):',
'- duplicateOf: 重複ルール ID(例: eslint/no-console, custom/my-rule',
'- duplicateLevel: exact / overlap / none',
@@ -125,8 +113,6 @@ const PROMPTS: Record<Language, {
'5. 重複がないルールは duplicateLevel: none とし、duplicateOf は付けない',
'6. 純粋な YAML を出力し、markdown コードブロックで囲まない',
],
existingTitle: '## 既存ルール',
noExisting: '(なし)',
userPrefix: '## 重複排除対象の YAML',
},
};
+57
View File
@@ -0,0 +1,57 @@
import staticRules from '../static-rules.json';
import type { CustomRule } from '../../types';
import { getLanguage, type Language } from '../../i18n/messages';
interface KnownRulesLabels {
header: string;
linterLabel: (name: string, count: number) => string;
customLabel: (count: number) => string;
footer: string;
}
const LABELS: Record<Language, KnownRulesLabels> = {
'zh-CN': {
header: '## 已知规则清单(用于重复检测)',
linterLabel: (name, count) => `### ${name} (${count} 条)`,
customLabel: (count) => `### 已导入的自定义规则 (${count} 条)`,
footer: '判定时请精确匹配上述规则 ID,而非模糊匹配分类。',
},
en: {
header: '## Known Rules (for duplicate detection)',
linterLabel: (name, count) => `### ${name} (${count} rules)`,
customLabel: (count) => `### Imported custom rules (${count} rules)`,
footer: 'Match exactly by rule ID above, not by fuzzy category matching.',
},
ja: {
header: '## 既知ルール一覧(重複検出用)',
linterLabel: (name, count) => `### ${name}${count} 件)`,
customLabel: (count) => `### インポート済みカスタムルール(${count} 件)`,
footer: '上記ルールIDで正確にマッチングしてください。曖昧なカテゴリマッチングは避けてください。',
},
};
export function buildKnownRulesSection(existingCustomRules?: CustomRule[]): string {
const lang = getLanguage();
const l = LABELS[lang] ?? LABELS['zh-CN'];
const lines: string[] = [l.header];
for (const [linter, rules] of Object.entries(staticRules.rules)) {
lines.push(l.linterLabel(linter, rules.length));
for (const rule of rules) {
lines.push(`- ${rule.id}: ${rule.description}`);
}
lines.push('');
}
if (existingCustomRules && existingCustomRules.length > 0) {
lines.push(l.customLabel(existingCustomRules.length));
for (const rule of existingCustomRules) {
lines.push(`- custom/${rule.id}: ${rule.description}`);
}
lines.push('');
}
lines.push(l.footer);
return lines.join('\n');
}
+2 -44
View File
@@ -1,6 +1,6 @@
import staticRules from '../static-rules.json';
import type { CustomRule } from '../../types';
import { getLanguage } from '../../i18n/messages';
import { buildKnownRulesSection } from './known-rules';
export type PromptInputType = 'freeform' | 'spreadsheet';
@@ -24,10 +24,6 @@ interface PromptStrings {
staticAnalysisTitle: string;
staticAnalysisLines: string[];
finalInstruction: string;
dedupHeader: string;
dedupLinterLabel: (name: string, count: number) => string;
dedupCustomLabel: (count: number) => string;
dedupFooter: string;
outputLang: string;
}
@@ -145,10 +141,6 @@ const p: Record<Lang, PromptStrings> = {
'仅输出 YAML,不要额外说明。',
],
finalInstruction: '只输出 YAML 内容,不要输出 markdown 代码块标记,不要输出解释性文字',
dedupHeader: '## 已知规则清单(用于重复检测)',
dedupLinterLabel: (name, count) => `### ${name} (${count} 条)`,
dedupCustomLabel: (count) => `### 已导入的自定义规则 (${count} 条)`,
dedupFooter: '判定时请精确匹配上述规则 ID,而非模糊匹配分类。',
outputLang: '输出语言:zh-CN',
},
@@ -265,10 +257,6 @@ const p: Record<Lang, PromptStrings> = {
'Output YAML only, no extra explanation.',
],
finalInstruction: 'All descriptions and messages must be written in English.\nOutput YAML only, no markdown code fences, no explanatory text',
dedupHeader: '## Known Rules (for duplicate detection)',
dedupLinterLabel: (name, count) => `### ${name} (${count} rules)`,
dedupCustomLabel: (count) => `### Imported custom rules (${count} rules)`,
dedupFooter: 'Match exactly by rule ID above, not by fuzzy category matching.',
outputLang: 'Output language: en',
},
@@ -385,10 +373,6 @@ const p: Record<Lang, PromptStrings> = {
'YAMLのみを出力し、追加説明は不要です。',
],
finalInstruction: 'すべてのdescriptionとmessageは日本語で出力してください。\nYAML のみ出力、マークダウンコードブロックなし、説明テキストなし',
dedupHeader: '## 既知ルール一覧(重複検出用)',
dedupLinterLabel: (name, count) => `### ${name}${count} 件)`,
dedupCustomLabel: (count) => `### インポート済みカスタムルール(${count} 件)`,
dedupFooter: '上記ルールIDで正確にマッチングしてください。曖昧なカテゴリマッチングは避けてください。',
outputLang: '出力言語:ja',
},
};
@@ -399,32 +383,6 @@ function getLang(): Lang {
return 'zh-CN';
}
function buildDedupPromptSection(existingCustomRules?: CustomRule[], lang?: Lang): string {
const l = lang ?? getLang();
const s = p[l];
const lines: string[] = [s.dedupHeader];
for (const [linter, rules] of Object.entries(staticRules.rules)) {
lines.push(s.dedupLinterLabel(linter, rules.length));
for (const rule of rules) {
lines.push(`- ${rule.id}: ${rule.description}`);
}
lines.push('');
}
if (existingCustomRules && existingCustomRules.length > 0) {
lines.push(s.dedupCustomLabel(existingCustomRules.length));
for (const rule of existingCustomRules) {
lines.push(`- custom/${rule.id}: ${rule.description}`);
}
lines.push('');
}
lines.push(s.dedupFooter);
return lines.join('\n');
}
export function buildSystemPrompt(inputType: PromptInputType, existingRules?: CustomRule[]): string {
const lang = getLang();
const s = p[lang];
@@ -460,7 +418,7 @@ export function buildSystemPrompt(inputType: PromptInputType, existingRules?: Cu
`${lang === 'zh-CN' ? '输出' : lang === 'ja' ? '出力' : 'Output'}`,
s.example2Output,
'',
buildDedupPromptSection(existingRules, lang),
buildKnownRulesSection(existingRules),
'',
s.staticAnalysisTitle,
...s.staticAnalysisLines,
+21 -3
View File
@@ -43,14 +43,30 @@ export function parseTemplate(srcPath: string): TemplateParseResult {
const totalRows = rows.length;
const rules: ImportableRule[] = rows
.map((r, idx) => ({ r, rowNo: idx + 2 }))
.filter(({ r }) => String(r.id ?? '').trim() !== '')
.filter(({ r }) => {
const id = String(r.id ?? '').trim();
const description = String(r.description ?? '').trim();
const message = String(r.message ?? '').trim();
return id !== '' || description !== '' || message !== '';
})
.map(({ r, rowNo }) => {
const issues: ValidationIssue[] = [];
const rawId = String(r.id ?? '').trim();
let id = rawId;
let idPlaceholder = false;
if (!rawId) {
id = `rule-${rowNo}`;
idPlaceholder = true;
issues.push({ field: 'id', severity: 'error', message: t('import.idMissing') });
}
const sevRaw = String(r.severity ?? '').trim().toLowerCase();
const originalSeverity = String(r.severity ?? '').trim();
const severity: Severity = VALID_SEVERITY.includes(sevRaw) ? (sevRaw as Severity) : 'warning';
if (!VALID_SEVERITY.includes(sevRaw)) {
issues.push({ field: 'severity', severity: 'warning', message: `severity 非法: "${r.severity ?? ''}"` });
const detail = originalSeverity ? `: "${originalSeverity}"` : '';
issues.push({ field: 'severity', severity: 'warning', message: `${t('import.validationSeverityInvalid')}${detail}` });
}
const description = String(r.description ?? '').trim();
@@ -64,13 +80,15 @@ export function parseTemplate(srcPath: string): TemplateParseResult {
}
return {
id: String(r.id).trim(),
id,
severity,
description,
message,
languages: splitList(r.languages),
excludeLanguages: splitList(r.excludeLanguages),
rowNumber: rowNo,
originalSeverity,
idPlaceholder,
validationIssues: issues.length > 0 ? issues : undefined,
};
});
+636 -140
View File
@@ -1,9 +1,13 @@
import * as vscode from 'vscode';
import type { ConversionResult, PreviewDecision, ImportableRule } from './import-types';
import { t } from '../i18n/messages';
import type { ConversionResult, PreviewDecision, ImportableRule, ValidationIssue } from './import-types';
import { dedupSingleRule } from './import-service';
import { loadActiveRules } from './yaml-parser';
import staticRules from './static-rules.json';
import { t, getLanguage } from '../i18n/messages';
export async function showImportPreview(
result: ConversionResult,
context: vscode.ExtensionContext,
): Promise<PreviewDecision | null> {
return new Promise((resolve) => {
const panel = vscode.window.createWebviewPanel(
@@ -22,9 +26,11 @@ export async function showImportPreview(
panel.webview.html = renderPreviewHtml(result, keepRule);
panel.webview.onDidReceiveMessage((msg) => {
panel.webview.onDidReceiveMessage(async (msg) => {
if (msg.type === 'toggleRule') {
keepRule[msg.ruleId] = msg.keep;
} else if (msg.type === 'addErrorRule') {
await handleAddErrorRule(msg, result, keepRule, context, panel);
} else if (msg.type === 'confirm') {
resolve({
keepRule,
@@ -42,6 +48,108 @@ export async function showImportPreview(
});
}
interface RuleValidationError {
field: 'id' | 'severity' | 'description' | 'message';
message: string;
}
function validateRule(rule: ImportableRule): RuleValidationError | null {
if (!rule.id || !rule.id.trim()) {
return { field: 'id', message: t('import.validationIdEmpty') };
}
if (!['error', 'warning', 'info'].includes(rule.severity)) {
return { field: 'severity', message: t('import.validationSeverityInvalid') };
}
if (!rule.description || !rule.description.trim()) {
return { field: 'description', message: t('import.validationDescEmpty', { 0: rule.id }) };
}
if (!rule.message || !rule.message.trim()) {
return { field: 'message', message: t('import.validationMsgEmpty', { 0: rule.id }) };
}
return null;
}
async function handleAddErrorRule(
msg: {
ruleId: string;
rule: ImportableRule;
},
result: ConversionResult,
keepRule: Record<string, boolean>,
context: vscode.ExtensionContext,
panel: vscode.WebviewPanel,
): Promise<void> {
const rule = msg.rule;
const validationError = validateRule(rule);
if (validationError) {
panel.webview.postMessage({
type: 'addError',
ruleId: msg.ruleId,
field: validationError.field,
message: validationError.message,
});
return;
}
const original = result.rules.find(r => r.id === msg.ruleId);
if (original?.idPlaceholder && rule.id === msg.ruleId) {
panel.webview.postMessage({
type: 'addError',
ruleId: msg.ruleId,
field: 'id',
message: t('import.idMissing'),
});
return;
}
const conflict = result.rules.some(r =>
!r.validationIssues?.length && r.id.toLowerCase() === rule.id.toLowerCase()
);
if (conflict) {
panel.webview.postMessage({
type: 'addError',
ruleId: msg.ruleId,
field: 'id',
message: t('import.idConflict', { 0: rule.id }),
});
return;
}
const dedup = await dedupSingleRule(rule, context);
const level = dedup?.duplicateLevel ?? 'none';
const dedupFailed = !dedup;
const idx = result.rules.findIndex(r => r.id === msg.ruleId);
if (idx >= 0) {
result.rules[idx] = {
...result.rules[idx],
id: rule.id,
severity: rule.severity,
description: rule.description,
message: rule.message,
languages: rule.languages,
excludeLanguages: rule.excludeLanguages,
duplicateLevel: level,
duplicateOf: dedup?.duplicateOf,
duplicateReason: dedup?.duplicateReason,
validationIssues: undefined,
};
}
keepRule[rule.id] = level !== 'exact';
panel.webview.postMessage({
type: 'ruleAdded',
ruleId: msg.ruleId,
id: rule.id,
duplicateLevel: level,
duplicateOf: dedup?.duplicateOf,
duplicateReason: dedup?.duplicateReason,
dedupFailed,
});
}
const SEVERITY_OPTIONS = ['error', 'warning', 'info'];
const SEVERITY_COLORS: Record<string, string> = {
error: '#f48771',
@@ -69,92 +177,151 @@ function renderPreviewHtml(
? `<div class="summary-bar" style="border-color:rgba(88,166,255,0.3);color:#58a6ff;">${t('import.template.skipped', { 0: String(result.skippedCount) })}</div>`
: '';
const hasValidRules = cleanRules.length > 0;
const emptyValidHint = !hasValidRules
? `<div class="validation-error" style="display:block;">${t('import.emptyValidRules')}</div>`
: '';
function dupLabel(dupOf: string | undefined): string {
return dupOf?.startsWith('custom/')
? `${t('import.customRulePrefix')} ${dupOf.slice(7)}`
: (dupOf ?? 'unknown');
}
const confirmBtnAttrs = hasValidRules
? 'onclick="doConfirm()"'
: 'disabled style="opacity:0.5;cursor:not-allowed;"';
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
const customRules = workspaceRoot ? loadActiveRules(workspaceRoot) : [];
function renderRuleCard(rule: ImportableRule): string {
function resolveDupDescription(dupOf: string | undefined): string | undefined {
if (!dupOf) { return undefined; }
if (dupOf.startsWith('custom/')) {
const id = dupOf.slice(7);
return customRules.find(r => r.id === id)?.description;
}
const slash = dupOf.indexOf('/');
const linter = slash > 0 ? dupOf.slice(0, slash) : '';
const linterRules = (staticRules.rules as Record<string, Array<{ id: string; description: string; descriptionZh?: string; descriptionJa?: string }>>)[linter];
const rule = linterRules?.find(r => r.id === dupOf);
if (!rule) { return undefined; }
const lang = getLanguage();
if (lang === 'zh-CN' && rule.descriptionZh) {
return `${rule.description} (${rule.descriptionZh})`;
}
if (lang === 'ja' && rule.descriptionJa) {
return `${rule.description} (${rule.descriptionJa})`;
}
return rule.description;
}
function renderRuleCard(rule: ImportableRule, isError = false): string {
const kept = keepRule[rule.id];
const color = SEVERITY_COLORS[rule.severity] || '#8b949e';
const editRule = rule;
const sevIssue = !!rule.validationIssues?.some(i => i.field === 'severity');
const color = sevIssue ? '#f48771' : (SEVERITY_COLORS[rule.severity] || '#8b949e');
let duplicateInfo = '';
if (rule.duplicateLevel === 'exact') {
const prefix = rule.duplicateOf?.startsWith('custom/') ? `${t('import.customRulePrefix')} ${rule.duplicateOf.slice(7)}` : (rule.duplicateOf ?? 'unknown');
duplicateInfo = `<div style="color:#8b949e;font-size:12px;margin-top:4px;">${t('import.duplicateOf', { 0: prefix })}</div>`;
} else if (rule.duplicateLevel === 'overlap') {
const prefix = rule.duplicateOf?.startsWith('custom/') ? `${t('import.customRulePrefix')} ${rule.duplicateOf.slice(7)}` : (rule.duplicateOf ?? 'unknown');
duplicateInfo = `
<div style="color:#d29922;font-size:12px;margin-top:4px;">${t('import.overlapWith', { 0: prefix })}</div>
${rule.duplicateReason ? `<div style="color:#8b949e;font-size:12px;margin-top:2px;">${t('import.overlapReason', { 0: rule.duplicateReason })}</div>` : ''}
`;
let statusBadge = '';
if (!isError) {
if (rule.duplicateLevel === 'exact') {
const dupDesc = resolveDupDescription(rule.duplicateOf);
duplicateInfo = `
<div class="dup-banner dup-exact">
<div class="dup-banner-title">${t('import.dupExactTitle')}</div>
<div class="dup-banner-text">${t('import.dupExactText', { 0: dupLabel(rule.duplicateOf) })}</div>
${dupDesc ? `<div class="dup-banner-desc">${t('import.dupDescriptionLabel', { 0: dupDesc })}</div>` : ''}
<div class="dup-banner-hint">${t('import.dupExactHint')}</div>
</div>
`;
statusBadge = `<span class="badge badge-exact">${kept ? t('import.badgeRestored') : t('import.badgeWillComment')}</span>`;
} else if (rule.duplicateLevel === 'overlap') {
const dupDesc = resolveDupDescription(rule.duplicateOf);
duplicateInfo = `
<div class="dup-banner dup-overlap">
<div class="dup-banner-title">${t('import.dupOverlapTitle')}</div>
<div class="dup-banner-text">${t('import.dupOverlapText', { 0: dupLabel(rule.duplicateOf) })}</div>
${dupDesc ? `<div class="dup-banner-desc">${t('import.dupDescriptionLabel', { 0: dupDesc })}</div>` : ''}
${rule.duplicateReason ? `<div class="dup-banner-reason">${t('import.overlapReason', { 0: rule.duplicateReason })}</div>` : ''}
</div>
`;
statusBadge = `<span class="badge badge-overlap">${kept ? t('importPreview.keep') : t('importPreview.comment')}</span>`;
} else {
statusBadge = `<span class="badge badge-none">${t('importPreview.keep')}</span>`;
}
} else {
statusBadge = `<span class="badge" style="background:rgba(248,81,73,0.15);color:#f48771;">${t('import.cannotImport')}</span>`;
}
const statusBadge = rule.duplicateLevel === 'exact'
? `<span class="badge badge-exact">${kept ? t('import.badgeRestored') : t('import.badgeWillComment')}</span>`
: rule.duplicateLevel === 'overlap'
? `<span class="badge badge-overlap">${kept ? t('importPreview.keep') : t('importPreview.comment')}</span>`
: `<span class="badge badge-none">${t('importPreview.keep')}</span>`;
const tagDisplay = (tags: string[] | undefined) => tags && tags.length > 0 ? tags.map(tag => `<span class="tag" data-value="${tag}">${tag}<span class="tag-remove" data-tag="${tag}">×</span></span>`).join('') : '';
const tagValue = (tags: string[] | undefined) => tags && tags.length > 0 ? tags.join(',') : '';
const tagDisplay = (tags: string[] | undefined) => tags && tags.length > 0 ? tags.map(t => `<span class="tag" data-value="${t}">${t}<span class="tag-remove" data-tag="${t}">×</span></span>`).join('') : '';
const issueByField = new Map<string, ValidationIssue>();
if (isError) {
for (const i of rule.validationIssues || []) {
if (!issueByField.has(i.field)) {
issueByField.set(i.field, i);
}
}
}
const issueCls = (field: string) => issueByField.has(field) ? ' field-error' : '';
const inputCls = (field: string) => issueByField.has(field) ? 'field-error-input ' : '';
const issueMsg = (field: string) => issueByField.has(field)
? `<div class="field-error-msg">${t('import.issuePrefix')} ${issueByField.get(field)!.message}</div>`
: '';
const actionArea = isError
? `<button class="add-btn" data-addbtn="${rule.id}" onclick="event.stopPropagation();addErrorRule(this)">${t('import.add')}</button>`
: `<div class="keep-toggle">
<button class="toggle-btn ${kept ? 'active' : ''}" data-action="keep" onclick="event.stopPropagation();toggleKeep(this, true)">${t('importPreview.keep')}</button>
<button class="toggle-btn ${!kept ? 'active' : ''}" data-action="comment" onclick="event.stopPropagation();toggleKeep(this, false)">${t('importPreview.comment')}</button>
</div>`;
const bodyDisplay = isError ? 'block' : 'none';
const expandIcon = '▼';
return `
<div class="rule-card" data-ruleid="${rule.id}">
<div class="rule-card-header" onclick="toggleCard('${rule.id}')">
<div class="rule-card${isError ? ' expanded' : ''}" data-ruleid="${rule.id}"${isError ? ' data-error="true"' : ''}>
<div class="rule-card-header" onclick="toggleCard(this)">
<div class="rule-card-summary">
<input class="rule-id-input${/^rule-\d+$/.test(rule.id) ? ' placeholder-id' : ''}" value="${rule.id}" onchange="syncId('${rule.id}', this.value)" onclick="event.stopPropagation()">
<span class="rule-severity-tag" style="background:${color}20;color:${color};border:1px solid ${color}40;">${editRule.severity}</span>
<span class="rule-desc-preview">${editRule.description}</span>
<input class="rule-id-input${/^rule-\d+$/.test(rule.id) ? ' placeholder-id' : ''}" value="${rule.id}" onchange="syncId(this)" onclick="event.stopPropagation()">
<span class="rule-severity-tag" style="background:${color}20;color:${color};border:1px solid ${color}40;">${sevIssue ? (rule.originalSeverity || t('import.severityMissing')) : rule.severity}</span>
<span class="rule-desc-preview">${rule.description}</span>
</div>
<div class="rule-card-meta">
${statusBadge}
<span class="expand-icon"></span>
<span class="expand-icon">${expandIcon}</span>
</div>
</div>
<div class="rule-card-body" id="body-${rule.id}" style="display:none;">
<div class="rule-card-body" id="body-${rule.id}" style="display:${bodyDisplay};">
<div class="edit-header">
<div class="id-row">
<span class="id-display-label">${t('import.idLabel')}</span>
<input class="id-display-input${/^rule-\d+$/.test(rule.id) ? ' placeholder-id' : ''}" value="${rule.id}" onchange="syncId('${rule.id}', this.value)">
<input class="id-display-input${/^rule-\d+$/.test(rule.id) ? ' placeholder-id' : ''}" value="${rule.id}" onchange="syncId(this)">
${/^rule-\d+$/.test(rule.id) ? `<span class="placeholder-hint">${t('import.placeholderIdHint')}</span>` : ''}
</div>
<div class="keep-toggle">
<button class="toggle-btn ${kept ? 'active' : ''}" data-action="keep" onclick="event.stopPropagation();toggleKeep('${rule.id}', true)">${t('importPreview.keep')}</button>
<button class="toggle-btn ${!kept ? 'active' : ''}" data-action="comment" onclick="event.stopPropagation();toggleKeep('${rule.id}', false)">${t('importPreview.comment')}</button>
</div>
${actionArea}
</div>
${duplicateInfo}
<div class="edit-field">
<div class="edit-field${issueCls('severity')}">
<label>${t('import.severityLabel')}</label>
<select onchange="updateRule('${rule.id}','severity',this.value)">
${SEVERITY_OPTIONS.map(s => `<option value="${s}" ${s === editRule.severity ? 'selected' : ''}>${s}</option>`).join('')}
<select class="${inputCls('severity')}" onchange="updateRule(this,'severity',this.value)">
${sevIssue ? `<option value="" disabled selected>${t('import.severitySelectHint')}</option>` : ''}
${SEVERITY_OPTIONS.map(s => `<option value="${s}" ${!sevIssue && s === rule.severity ? 'selected' : ''}>${s}</option>`).join('')}
</select>
${issueMsg('severity')}
</div>
<div class="edit-field">
<div class="edit-field${issueCls('description')}">
<label>${t('import.descriptionLabel')}</label>
<textarea rows="2" onchange="updateRule('${rule.id}','description',this.value)">${editRule.description}</textarea>
<textarea rows="2" class="${inputCls('description')}" onchange="updateRule(this,'description',this.value)">${rule.description}</textarea>
${issueMsg('description')}
</div>
<div class="edit-field">
<div class="edit-field${issueCls('message')}">
<label>${t('import.messageLabel')}</label>
<textarea rows="2" onchange="updateRule('${rule.id}','message',this.value)">${editRule.message}</textarea>
<textarea rows="2" class="${inputCls('message')}" onchange="updateRule(this,'message',this.value)">${rule.message}</textarea>
${issueMsg('message')}
</div>
<div class="edit-field">
<label>${t('import.languagesLabel')}</label>
<div class="tag-input-wrapper">
<div class="tag-list" data-ruleid="${rule.id}" data-field="languages">
${tagDisplay(editRule.languages)}
${tagDisplay(rule.languages)}
</div>
<input class="tag-input" data-ruleid="${rule.id}" data-field="languages" placeholder="${t('import.tagPlaceholder')}" value="">
</div>
@@ -164,7 +331,7 @@ function renderPreviewHtml(
<label>${t('import.excludeLanguagesLabel')}</label>
<div class="tag-input-wrapper">
<div class="tag-list" data-ruleid="${rule.id}" data-field="excludeLanguages">
${tagDisplay(editRule.excludeLanguages)}
${tagDisplay(rule.excludeLanguages)}
</div>
<input class="tag-input" data-ruleid="${rule.id}" data-field="excludeLanguages" placeholder="${t('import.tagPlaceholder')}" value="">
</div>
@@ -174,59 +341,35 @@ function renderPreviewHtml(
`;
}
function renderErrorCard(rule: ImportableRule): string {
const issues = (rule.validationIssues || []).map(i =>
`<div style="color:#f48771;font-size:12px;margin-bottom:4px;">${t('import.issuePrefix')} ${i.message}</div>`
).join('');
return `
<div class="rule-card" data-error="true" style="opacity:0.7;border-color:rgba(248,81,73,0.3);">
<div class="rule-card-header" style="cursor:default;">
<div class="rule-card-summary">
<span style="font-family:monospace;font-size:13px;font-weight:600;">${rule.id}</span>
<span style="color:#f48771;font-size:11px;font-weight:600;">${t('import.cannotImport')}</span>
</div>
</div>
<div class="rule-card-body" style="border-top:1px solid rgba(248,81,73,0.15);padding-top:8px;">
${issues}
<div style="color:#8b949e;font-size:11px;margin-top:6px;">
severity: ${rule.severity} | description: ${rule.description} | message: ${rule.message}
</div>
</div>
</div>
`;
}
function renderErrorSection(rules: ImportableRule[]): string {
if (rules.length === 0) { return ''; }
const sectionId = 'section-error';
return `
<div style="margin-bottom:12px;">
<div class="section-wrapper expanded" data-section-wrap="error" style="margin-bottom:12px;${rules.length === 0 ? 'display:none;' : ''}">
<div class="section-header" onclick="toggleSection('${sectionId}')">
<span style="font-size:14px;">🚫</span>
<span class="section-title">${t('import.sectionInvalid')}${rules.length}</span>
<span class="section-title" data-section-title="error"></span>
<span class="section-arrow">▼</span>
</div>
<div id="${sectionId}">
${rules.map(renderErrorCard).join('')}
<div id="${sectionId}" data-section="error" style="display:block;">
${rules.map(r => renderRuleCard(r, true)).join('')}
</div>
</div>
`;
}
function renderSection(title: string, icon: string, rules: ImportableRule[], _defaultExpanded: boolean): string {
if (rules.length === 0) { return ''; }
const sectionId = `section-${title.replace(/\s/g, '')}`;
function renderSection(title: string, icon: string, key: string, rules: ImportableRule[]): string {
const sectionId = `section-${key}`;
const count = rules.length;
const show = rules.some(r => keepRule[r.id] !== undefined);
return `
<div style="margin-bottom:12px;">
<div class="section-wrapper${show ? ' expanded' : ''}" data-section-wrap="${key}" style="margin-bottom:12px;${count === 0 ? 'display:none;' : ''}">
<div class="section-header" onclick="toggleSection('${sectionId}')">
<span style="font-size:14px;">${icon}</span>
<span class="section-title">${title}${t('import.ruleCount', { 0: rules.length })}</span>
<span class="section-arrow"></span>
<span class="section-title" data-section-title="${key}"></span>
<span class="section-arrow"></span>
</div>
<div id="${sectionId}" style="display:${show ? 'block' : 'none'};">
${rules.map(renderRuleCard).join('')}
<div id="${sectionId}" data-section="${key}" style="display:${show ? 'block' : 'none'};">
${rules.map(r => renderRuleCard(r)).join('')}
</div>
</div>
`;
@@ -265,6 +408,10 @@ body {
font-size: 12px; background: rgba(139,92,246,0.1);
border: 1px solid rgba(139,92,246,0.3); color: #a78bfa;
}
.summary-bar.warn {
border-color: rgba(210,153,34,0.3); color: #d29922;
background: rgba(210,153,34,0.1);
}
.actions {
display: flex; gap: 8px; padding-top: 12px;
border-top: 1px solid var(--vscode-panel-border);
@@ -284,7 +431,13 @@ body {
cursor: pointer; padding: 4px 0;
}
.section-title { font-weight: 600; font-size: 13px; }
.section-arrow { font-size: 10px; color: var(--vscode-descriptionForeground); }
.section-arrow {
display: inline-block;
font-size: 10px;
color: var(--vscode-descriptionForeground);
transform: rotate(-90deg);
transition: transform 0.15s ease;
}
.rule-card {
border: 1px solid var(--vscode-panel-border);
border-radius: 8px; margin-bottom: 8px; overflow: hidden;
@@ -324,7 +477,17 @@ body {
.rule-card-meta {
display: flex; align-items: center; gap: 8px; flex-shrink: 0;
}
.expand-icon { font-size: 10px; color: var(--vscode-descriptionForeground); }
.expand-icon {
display: inline-block;
font-size: 10px;
color: var(--vscode-descriptionForeground);
transform: rotate(-90deg);
transition: transform 0.15s ease;
}
.rule-card.expanded .expand-icon,
.section-wrapper.expanded .section-arrow {
transform: rotate(0deg);
}
.badge {
padding: 1px 8px; border-radius: 10px; font-size: 11px; font-weight: 600;
}
@@ -339,7 +502,10 @@ body {
padding: 10px 0 8px;
}
.id-row {
display: flex; align-items: center; gap: 8px;
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.id-row .field-error-msg {
flex-basis: 100%; margin-left: 0;
}
.keep-toggle { display: flex; gap: 4px; }
.toggle-btn {
@@ -353,6 +519,20 @@ body {
.toggle-btn.active[data-action="comment"] {
background: rgba(248,81,73,0.15); color: #f48771; border-color: rgba(248,81,73,0.3);
}
.add-btn {
padding: 4px 14px; border-radius: 4px; cursor: pointer; font-size: 11px;
border: 1px solid rgba(35,134,54,0.4);
background: rgba(35,134,54,0.15); color: #3fb950;
}
.add-btn:hover { background: rgba(35,134,54,0.25); }
.add-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.field-error-input {
border-color: rgba(248,81,73,0.7) !important;
box-shadow: 0 0 0 1px rgba(248,81,73,0.25);
}
.field-error-msg {
color: #f48771; font-size: 11px; margin-top: 4px;
}
.edit-field { margin-top: 10px; }
.edit-field label {
display: block; font-size: 11px; font-weight: 600;
@@ -420,6 +600,26 @@ body {
background: rgba(248,81,73,0.15); color: #f48771;
border: 1px solid rgba(248,81,73,0.3); font-size: 12px;
}
.dup-banner {
border-radius: 6px; padding: 8px 12px; margin-top: 10px;
font-size: 12px; line-height: 1.6;
}
.dup-exact {
background: rgba(248,81,73,0.1);
border: 1px solid rgba(248,81,73,0.3);
border-left: 3px solid #f48771;
}
.dup-overlap {
background: rgba(210,153,34,0.1);
border: 1px solid rgba(210,153,34,0.3);
border-left: 3px solid #d29922;
}
.dup-banner-title { font-weight: 600; }
.dup-exact .dup-banner-title { color: #f48771; }
.dup-overlap .dup-banner-title { color: #d29922; }
.dup-banner-desc { color: var(--vscode-foreground); }
.dup-banner-reason { color: #d29922; }
.dup-banner-hint { color: var(--vscode-descriptionForeground); }
</style>
</head>
<body>
@@ -428,40 +628,73 @@ body {
<div class="header-sub">${t('importPreview.source', { 0: result.sourceFileName, 1: String(result.rules.length) })}</div>
</div>
<div class="summary">
<div class="summary-item" style="border-color:rgba(248,81,73,0.3);color:#f48771;">${t('import.exactDuplicate', { 0: exactRules.length })}</div>
<div class="summary-item" style="border-color:rgba(210,153,34,0.3);color:#d29922;">${t('import.overlapDuplicate', { 0: overlapRules.length })}</div>
<div class="summary-item" style="border-color:rgba(35,134,54,0.3);color:#3fb950;">${t('import.noDuplicate', { 0: noneRules.length })}</div>
<div class="summary-item" style="border-color:rgba(248,81,73,0.3);color:#f48771;">${t('import.exactDuplicate', { 0: `<span id="count-exact">${exactRules.length}</span>` })}</div>
<div class="summary-item" style="border-color:rgba(210,153,34,0.3);color:#d29922;">${t('import.overlapDuplicate', { 0: `<span id="count-overlap">${overlapRules.length}</span>` })}</div>
<div class="summary-item" style="border-color:rgba(35,134,54,0.3);color:#3fb950;">${t('import.noDuplicate', { 0: `<span id="count-none">${noneRules.length}</span>` })}</div>
</div>
<div class="summary-bar" id="statusBar">
${t('import.statusBar', { 0: `<b id="keepCount">${totalKept}</b>`, 1: `<b id="commentCount">${totalCommented}</b>` })}
<span id="editHint" style="display:none;">${t('import.editedHint', { 0: '<b id="editCount">0</b>' })}</span>
</div>
<div id="addHint" class="summary-bar warn" style="display:none;"></div>
<div id="validationError" class="validation-error" style="display:none;"></div>
${skippedHint}
${renderErrorSection(errorRules)}
${renderSection(t('import.sectionExact'), '⛔', exactRules, false)}
${renderSection(t('import.sectionOverlap'), '⚠️', overlapRules, true)}
${renderSection(t('import.sectionNone'), '✅', noneRules, false)}
${renderSection(t('import.sectionExact'), '⛔', 'exact', exactRules)}
${renderSection(t('import.sectionOverlap'), '⚠️', 'overlap', overlapRules)}
${renderSection(t('import.sectionNone'), '✅', 'none', noneRules)}
${emptyValidHint}
<div id="emptyValidHint" class="validation-error" style="display:none;">${t('import.emptyValidRules')}</div>
<div class="actions">
<button class="btn" onclick="cancel()">${t('importPreview.cancel')}</button>
<button class="btn btn-primary" ${confirmBtnAttrs}>${t('importPreview.confirm')}</button>
<button class="btn btn-primary" id="confirmBtn" onclick="doConfirm()">${t('importPreview.confirm')}</button>
</div>
<script>
const vscode = acquireVsCodeApi();
const editedRules = {};
let addedRules = 0;
let addingRuleId = null;
const VALIDATION_DESC_EMPTY = ${JSON.stringify(t('import.validationDescEmpty'))};
const VALIDATION_MSG_EMPTY = ${JSON.stringify(t('import.validationMsgEmpty'))};
const VALIDATION_ID_EMPTY = ${JSON.stringify(t('import.validationIdEmpty'))};
const ADD_TEXT = ${JSON.stringify(t('import.add'))};
const ADDING_TEXT = ${JSON.stringify(t('import.adding'))};
const KEEP_TEXT = ${JSON.stringify(t('importPreview.keep'))};
const COMMENT_TEXT = ${JSON.stringify(t('importPreview.comment'))};
const WILL_COMMENT_TEXT = ${JSON.stringify(t('import.badgeWillComment'))};
const DEDUP_FALLBACK_TEXT = ${JSON.stringify(t('import.addDedupFallback'))};
const CUSTOM_PREFIX = ${JSON.stringify(t('import.customRulePrefix'))};
const DUPLICATE_OF_TEXT = ${JSON.stringify(t('import.duplicateOf'))};
const OVERLAP_WITH_TEXT = ${JSON.stringify(t('import.overlapWith'))};
const OVERLAP_REASON_TEXT = ${JSON.stringify(t('import.overlapReason'))};
const RULE_COUNT_TEMPLATE = ${JSON.stringify(t('import.ruleCount'))};
const SECTION_TITLES = {
error: ${JSON.stringify(t('import.sectionInvalid'))},
exact: ${JSON.stringify(t('import.sectionExact'))},
overlap: ${JSON.stringify(t('import.sectionOverlap'))},
none: ${JSON.stringify(t('import.sectionNone'))},
};
function syncId(originalId, newValue) {
const card = document.querySelector('.rule-card[data-ruleid="' + originalId + '"]');
function setSectionTitle(key, count) {
const el = document.querySelector('[data-section-title="' + key + '"]');
if (el) {
el.textContent = SECTION_TITLES[key] + '' + RULE_COUNT_TEMPLATE.replace('{0}', count) + '';
}
}
function fmt(tpl, v) {
return tpl.replace('{0}', v);
}
function syncId(el) {
const card = el.closest('.rule-card');
if (!card) return;
const originalId = card.dataset.ruleid;
const newValue = el.value;
const headerInput = card.querySelector('.rule-id-input');
const panelInput = card.querySelector('.id-display-input');
if (headerInput) headerInput.value = newValue;
@@ -472,48 +705,52 @@ function syncId(originalId, newValue) {
updateEditHint();
const isPlaceholder = /^rule-\\d+$/.test(newValue);
[headerInput, panelInput].forEach(el => {
if (el) {
el.classList.toggle('placeholder-id', isPlaceholder);
[headerInput, panelInput].forEach(input => {
if (input) {
input.classList.toggle('placeholder-id', isPlaceholder);
}
});
}
function toggleCard(ruleId) {
const body = document.getElementById('body-' + ruleId);
const card = body.closest('.rule-card');
const icon = card.querySelector('.expand-icon');
function toggleCard(el) {
const card = el.closest('.rule-card');
if (!card) return;
const body = card.querySelector('.rule-card-body');
if (body.style.display === 'none') {
body.style.display = 'block';
icon.textContent = '▲';
card.classList.add('expanded');
} else {
body.style.display = 'none';
icon.textContent = '▼';
card.classList.remove('expanded');
}
}
function toggleSection(id) {
const el = document.getElementById(id);
const arrow = el.previousElementSibling.querySelector('.section-arrow');
const wrap = el.closest('.section-wrapper');
if (el.style.display === 'none') {
el.style.display = 'block';
arrow.textContent = '▼';
wrap.classList.add('expanded');
} else {
el.style.display = 'none';
arrow.textContent = '▶';
wrap.classList.remove('expanded');
}
}
function toggleKeep(ruleId, keep) {
vscode.postMessage({ type: 'toggleRule', ruleId, keep });
const card = document.querySelector('.rule-card[data-ruleid="' + ruleId + '"]');
function toggleKeep(el, keep) {
const card = el.closest('.rule-card');
if (!card) return;
const ruleId = card.dataset.ruleid;
vscode.postMessage({ type: 'toggleRule', ruleId, keep });
const btns = card.querySelectorAll('.toggle-btn');
btns.forEach(b => b.classList.toggle('active', (keep && b.dataset.action === 'keep') || (!keep && b.dataset.action === 'comment')));
updateSummary();
}
function updateRule(ruleId, field, value) {
function updateRule(el, field, value) {
const card = el.closest('.rule-card');
if (!card) return;
const ruleId = card.dataset.ruleid;
if (!editedRules[ruleId]) {
editedRules[ruleId] = {};
}
@@ -521,34 +758,288 @@ function updateRule(ruleId, field, value) {
updateEditHint();
}
function collectCardRule(card) {
if (!card) return null;
const idInput = card.querySelector('.id-display-input');
const severityEl = card.querySelector('.edit-field select');
const textareas = card.querySelectorAll('.edit-field textarea');
const descEl = textareas[0];
const msgEl = textareas[1];
const langList = card.querySelector('.tag-list[data-field="languages"]');
const exclList = card.querySelector('.tag-list[data-field="excludeLanguages"]');
return {
id: idInput ? idInput.value.trim() : card.dataset.ruleid,
severity: severityEl ? severityEl.value : 'warning',
description: descEl ? descEl.value : '',
message: msgEl ? msgEl.value : '',
languages: langList ? Array.from(langList.querySelectorAll('.tag')).map(tag => tag.dataset.value) : [],
excludeLanguages: exclList ? Array.from(exclList.querySelectorAll('.tag')).map(tag => tag.dataset.value) : [],
};
}
function collectEditedRules() {
const result = [];
document.querySelectorAll('.rule-card').forEach(card => {
if (card.hasAttribute('data-error')) { return; }
const originalId = card.dataset.ruleid;
const idInput = card.querySelector('.id-display-input');
const ruleId = idInput ? idInput.value.trim() || originalId : originalId;
const severityEl = card.querySelector('.edit-field select');
const textareas = card.querySelectorAll('.edit-field textarea');
const descEl = textareas[0];
const msgEl = textareas[1];
const langList = card.querySelector('.tag-list[data-field="languages"]');
const exclList = card.querySelector('.tag-list[data-field="excludeLanguages"]');
const rule = {
id: ruleId,
severity: severityEl ? severityEl.value : 'warning',
description: descEl ? descEl.value : '',
message: msgEl ? msgEl.value : '',
languages: langList ? Array.from(langList.querySelectorAll('.tag')).map(t => t.dataset.value) : [],
excludeLanguages: exclList ? Array.from(exclList.querySelectorAll('.tag')).map(t => t.dataset.value) : [],
};
result.push(rule);
const rule = collectCardRule(card);
if (rule) { result.push(rule); }
});
return result;
}
function addErrorRule(el) {
if (addingRuleId) { return; }
const card = el.closest('.rule-card');
if (!card) return;
const btn = el;
if (btn.disabled) { return; }
btn.disabled = true;
btn.textContent = ADDING_TEXT;
const ruleId = card.dataset.ruleid;
addingRuleId = ruleId;
const rule = collectCardRule(card);
if (!rule) {
addingRuleId = null;
btn.disabled = false;
btn.textContent = ADD_TEXT;
return;
}
vscode.postMessage({ type: 'addErrorRule', ruleId, rule });
}
function fieldElement(card, field) {
if (field === 'id') return card.querySelector('.id-display-input');
if (field === 'severity') return card.querySelector('.edit-field select');
const tas = card.querySelectorAll('.edit-field textarea');
return field === 'description' ? (tas[0] || null) : (tas[1] || null);
}
function setFieldError(card, field, message) {
const el = fieldElement(card, field);
if (!el) return;
el.classList.add('field-error-input');
const wrap = el.closest('.edit-field, .id-row');
if (!wrap) return;
wrap.classList.add('field-error');
let msg = wrap.querySelector('.field-error-msg');
if (!msg) {
msg = document.createElement('div');
msg.className = 'field-error-msg';
wrap.appendChild(msg);
}
msg.textContent = '⚠ ' + message;
}
function clearFieldError(card, field) {
const el = fieldElement(card, field);
if (!el) return;
el.classList.remove('field-error-input');
const wrap = el.closest('.edit-field, .id-row');
if (wrap) {
wrap.classList.remove('field-error');
const msg = wrap.querySelector('.field-error-msg');
if (msg) { msg.remove(); }
}
}
function clearCardFieldErrors(card) {
card.querySelectorAll('.field-error-input').forEach(function (el) {
el.classList.remove('field-error-input');
});
card.querySelectorAll('.field-error').forEach(function (wrap) {
wrap.classList.remove('field-error');
const msg = wrap.querySelector('.field-error-msg');
if (msg) { msg.remove(); }
});
}
function liveClear(event) {
const card = event.target.closest('.rule-card');
if (!card || !card.hasAttribute('data-error')) return;
const target = event.target;
if (target.classList.contains('id-display-input') || target.classList.contains('rule-id-input')) {
if (target.value.trim()) { clearFieldError(card, 'id'); }
} else if (target.tagName === 'SELECT') {
clearFieldError(card, 'severity');
} else if (target.tagName === 'TEXTAREA') {
const tas = card.querySelectorAll('.edit-field textarea');
const field = tas[0] === target ? 'description' : (tas[1] === target ? 'message' : null);
if (field && target.value.trim()) { clearFieldError(card, field); }
}
}
function showCardError(ruleId, field, message) {
const card = document.querySelector('.rule-card[data-ruleid="' + ruleId + '"]');
if (card && field) {
setFieldError(card, field, message);
}
const btn = document.querySelector('[data-addbtn="' + ruleId + '"]');
if (btn) {
btn.disabled = false;
btn.textContent = ADD_TEXT;
}
addingRuleId = null;
}
function dupInfoHtml(level, dupOf, reason) {
const prefix = dupOf && dupOf.startsWith('custom/')
? CUSTOM_PREFIX + ' ' + dupOf.slice(7)
: (dupOf || 'unknown');
if (level === 'exact') {
return '<div style="color:#8b949e;font-size:12px;margin-top:4px;">' + fmt(DUPLICATE_OF_TEXT, prefix) + '</div>';
}
if (level === 'overlap') {
let html = '<div style="color:#d29922;font-size:12px;margin-top:4px;">' + fmt(OVERLAP_WITH_TEXT, prefix) + '</div>';
if (reason) {
html += '<div style="color:#8b949e;font-size:12px;margin-top:2px;">' + fmt(OVERLAP_REASON_TEXT, reason) + '</div>';
}
return html;
}
return '';
}
function moveCardToSection(msg) {
const card = document.querySelector('.rule-card[data-ruleid="' + msg.ruleId + '"]');
if (!card) return;
card.dataset.ruleid = msg.id;
const headerInput = card.querySelector('.rule-id-input');
const panelInput = card.querySelector('.id-display-input');
if (headerInput) headerInput.value = msg.id;
if (panelInput) panelInput.value = msg.id;
const isPlaceholder = /^rule-\\d+$/.test(msg.id);
[headerInput, panelInput].forEach(el => {
if (el) el.classList.toggle('placeholder-id', isPlaceholder);
});
card.removeAttribute('data-error');
clearCardFieldErrors(card);
const addBtn = card.querySelector('.add-btn');
if (addBtn) { addBtn.remove(); }
const kept = msg.duplicateLevel !== 'exact';
const toggle = document.createElement('div');
toggle.className = 'keep-toggle';
function makeToggleBtn(action, active, label) {
const btn = document.createElement('button');
btn.className = 'toggle-btn' + (active ? ' active' : '');
btn.dataset.action = action;
btn.textContent = label;
btn.addEventListener('click', function (ev) {
ev.stopPropagation();
toggleKeep(this, action === 'keep');
});
return btn;
}
toggle.appendChild(makeToggleBtn('keep', kept, KEEP_TEXT));
toggle.appendChild(makeToggleBtn('comment', !kept, COMMENT_TEXT));
card.querySelector('.edit-header').appendChild(toggle);
const meta = card.querySelector('.rule-card-meta');
const oldBadge = meta.querySelector('.badge');
if (oldBadge) { oldBadge.remove(); }
const badge = document.createElement('span');
badge.className = 'badge';
if (msg.duplicateLevel === 'exact') {
badge.classList.add('badge-exact');
badge.textContent = WILL_COMMENT_TEXT;
} else if (msg.duplicateLevel === 'overlap') {
badge.classList.add('badge-overlap');
badge.textContent = KEEP_TEXT;
} else {
badge.classList.add('badge-none');
badge.textContent = KEEP_TEXT;
}
const icon = meta.querySelector('.expand-icon');
meta.insertBefore(badge, icon);
const body = card.querySelector('.rule-card-body');
const infoHtml = dupInfoHtml(msg.duplicateLevel, msg.duplicateOf, msg.duplicateReason);
if (infoHtml) {
const infoDiv = document.createElement('div');
infoDiv.innerHTML = infoHtml;
const firstField = body.querySelector('.edit-field');
body.insertBefore(infoDiv, firstField);
}
const section = msg.duplicateLevel === 'exact'
? 'exact'
: (msg.duplicateLevel === 'overlap' ? 'overlap' : 'none');
const target = document.querySelector('[data-section="' + section + '"]');
if (target) {
const wrap = target.closest('[data-section-wrap]');
if (wrap) {
wrap.style.display = '';
wrap.classList.add('expanded');
}
target.style.display = 'block';
target.appendChild(card);
}
addedRules++;
if (msg.dedupFailed) {
showAddHint(DEDUP_FALLBACK_TEXT);
}
updateSectionCounts();
updateSummary();
updateEditHint();
}
function showAddHint(text) {
const el = document.getElementById('addHint');
el.textContent = text;
el.style.display = 'block';
setTimeout(function () { el.style.display = 'none'; }, 5000);
}
function updateSectionCounts() {
const sections = ['error', 'exact', 'overlap', 'none'];
const counts = {};
let totalValid = 0;
for (const key of sections) {
const container = document.querySelector('[data-section="' + key + '"]');
const count = container ? container.querySelectorAll('.rule-card').length : 0;
counts[key] = count;
if (key !== 'error') { totalValid += count; }
setSectionTitle(key, count);
if (container) {
const wrap = container.closest('[data-section-wrap]');
if (wrap) { wrap.style.display = count > 0 ? '' : 'none'; }
}
}
document.getElementById('count-exact').textContent = counts.exact;
document.getElementById('count-overlap').textContent = counts.overlap;
document.getElementById('count-none').textContent = counts.none;
const btn = document.getElementById('confirmBtn');
const hint = document.getElementById('emptyValidHint');
if (totalValid === 0) {
btn.disabled = true;
btn.style.opacity = '0.5';
btn.style.cursor = 'not-allowed';
hint.style.display = 'block';
} else {
btn.disabled = false;
btn.style.opacity = '';
btn.style.cursor = '';
hint.style.display = 'none';
}
}
window.addEventListener('message', function (e) {
const msg = e.data;
if (!msg) { return; }
if (msg.type === 'addError') {
showCardError(msg.ruleId, msg.field, msg.message);
} else if (msg.type === 'ruleAdded') {
moveCardToSection(msg);
}
});
function validate() {
const rules = collectEditedRules();
for (const rule of rules) {
@@ -575,7 +1066,8 @@ function doConfirm() {
}
const edited = collectEditedRules();
const hasEdits = Object.keys(editedRules).length > 0;
vscode.postMessage({ type: 'confirm', editedRules: hasEdits ? edited : undefined });
const withData = (hasEdits || addedRules > 0) ? edited : undefined;
vscode.postMessage({ type: 'confirm', editedRules: withData });
}
function cancel() {
@@ -586,7 +1078,6 @@ function updateSummary() {
let keepCount = 0, commentCount = 0;
document.querySelectorAll('.rule-card').forEach(card => {
if (card.hasAttribute('data-error')) { return; }
const ruleId = card.dataset.ruleid;
const keepBtns = card.querySelectorAll('.toggle-btn');
let isKept = true;
keepBtns.forEach(b => {
@@ -621,7 +1112,7 @@ document.addEventListener('keydown', function(e) {
const list = input.parentElement.querySelector('.tag-list');
const existing = list.querySelectorAll('.tag');
const exists = Array.from(existing).some(t => t.dataset.value === val);
const exists = Array.from(existing).some(tag => tag.dataset.value === val);
if (exists) { input.value = ''; return; }
const tag = document.createElement('span');
@@ -631,7 +1122,7 @@ document.addEventListener('keydown', function(e) {
list.appendChild(tag);
input.value = '';
const tags = Array.from(list.querySelectorAll('.tag')).map(t => t.dataset.value);
const tags = Array.from(list.querySelectorAll('.tag')).map(tag => tag.dataset.value);
if (!editedRules[ruleId]) editedRules[ruleId] = {};
editedRules[ruleId][field] = tags;
updateEditHint();
@@ -645,12 +1136,17 @@ document.addEventListener('click', function(e) {
const ruleId = list.dataset.ruleid;
const field = list.dataset.field;
tag.remove();
const remaining = Array.from(list.querySelectorAll('.tag')).map(t => t.dataset.value);
const remaining = Array.from(list.querySelectorAll('.tag')).map(tag => tag.dataset.value);
if (!editedRules[ruleId]) editedRules[ruleId] = {};
editedRules[ruleId][field] = remaining;
updateEditHint();
}
});
document.addEventListener('input', liveClear);
document.addEventListener('change', liveClear);
updateSectionCounts();
</script>
</body>
</html>`;
+63 -10
View File
@@ -24,6 +24,11 @@ interface ParsedYamlItem {
[key: string]: unknown;
}
function stripQuotes(raw: string): string {
const m = raw.match(/^(['"])(.*)\1$/);
return m ? m[2] : raw;
}
function parseSimpleYaml(content: string): ParsedYamlItem[] {
const items: ParsedYamlItem[] = [];
let current: ParsedYamlItem | null = null;
@@ -44,7 +49,7 @@ function parseSimpleYaml(content: string): ParsedYamlItem[] {
s.trim().replace(/^['"]|['"]$/g, '')
);
} else {
current[key] = raw;
current[key] = stripQuotes(raw);
}
}
} else if (current) {
@@ -59,7 +64,7 @@ function parseSimpleYaml(content: string): ParsedYamlItem[] {
s.trim().replace(/^['"]|['"]$/g, '')
);
} else {
current[key] = raw;
current[key] = stripQuotes(raw);
}
}
}
@@ -90,6 +95,45 @@ export function parseImportableYaml(content: string): ImportableRule[] {
}));
}
export interface DedupResult {
duplicateLevel: 'exact' | 'overlap' | 'none';
duplicateOf?: string;
duplicateReason?: string;
}
export async function dedupSingleRule(
rule: ImportableRule,
context: vscode.ExtensionContext,
): Promise<DedupResult | null> {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
const existingRules = workspaceRoot ? loadActiveRules(workspaceRoot) : [];
const singleYaml = [
`- id: ${rule.id}`,
` severity: ${rule.severity}`,
` description: ${rule.description}`,
` message: ${rule.message}`,
...(rule.languages?.length ? [` languages: [${rule.languages.join(', ')}]`] : []),
...(rule.excludeLanguages?.length ? [` excludeLanguages: [${rule.excludeLanguages.join(', ')}]`] : []),
].join('\n');
const { system, user } = buildDedupOnlyPrompt(singleYaml, existingRules);
for (let attempt = 0; attempt < 2; attempt++) {
const out = await convertContentWithAI(user, context, system, true);
if (!out) { continue; }
const parsed = parseImportableYaml(out);
if (parsed.length === 0) { continue; }
const r = parsed[0];
return {
duplicateLevel: r.duplicateLevel ?? 'none',
duplicateOf: r.duplicateOf,
duplicateReason: r.duplicateReason,
};
}
return null;
}
export function buildFinalYaml(
yamlContent: string,
rules: ImportableRule[],
@@ -405,15 +449,20 @@ export async function convertContentWithAI(
content: string,
context: vscode.ExtensionContext,
systemPrompt?: string,
quiet?: boolean,
): Promise<string | null> {
if (!content.trim()) {
vscode.window.showErrorMessage(t('import.emptyFile'));
if (!quiet) {
vscode.window.showErrorMessage(t('import.emptyFile'));
}
return null;
}
const apiKey = await getApiKey(context);
if (!apiKey) {
vscode.window.showErrorMessage(t('import.needApiKey'));
if (!quiet) {
vscode.window.showErrorMessage(t('import.needApiKey'));
}
return null;
}
@@ -432,11 +481,13 @@ export async function convertContentWithAI(
seed: 42,
});
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') {
vscode.window.showErrorMessage(t('import.timeout'));
} else {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(t('import.aiFail', { 0: msg }));
if (!quiet) {
if (err instanceof DOMException && err.name === 'AbortError') {
vscode.window.showErrorMessage(t('import.timeout'));
} else {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(t('import.aiFail', { 0: msg }));
}
}
return null;
}
@@ -447,7 +498,9 @@ export async function convertContentWithAI(
.trim();
if (!cleaned) {
vscode.window.showErrorMessage(t('import.emptyResponse'));
if (!quiet) {
vscode.window.showErrorMessage(t('import.emptyResponse'));
}
return null;
}
+2
View File
@@ -12,6 +12,8 @@ export interface ImportableRule extends CustomRule {
duplicateReason?: string;
validationIssues?: ValidationIssue[];
rowNumber?: number;
originalSeverity?: string;
idPlaceholder?: boolean;
}
export interface ConversionResult {
+1766 -597
View File
@@ -5,2045 +5,3214 @@
"ts-eslint": "8.x (35 rules)",
"stylelint": "16.x (68 rules)",
"pmd": "7.26.0 (274 Java rules + 12 JSP rules)",
"sql-lint": "4.2.2 (57 recommended)"
"sqlfluff": "4.2.2 (57 recommended)"
},
"rules": {
"eslint": [
{
"id": "eslint/constructor-super",
"description": "Verify calls of super() in constructors"
"description": "Verify calls of super() in constructors",
"descriptionZh": "在构造函数中校验 super() 的调用",
"descriptionJa": "コンストラクタで super() の呼び出しを検証する"
},
{
"id": "eslint/for-direction",
"description": "Enforce for loop update clause moving the counter in the right direction"
"description": "Enforce for loop update clause moving the counter in the right direction",
"descriptionZh": "确保 for 循环更新子句朝正确方向移动计数器",
"descriptionJa": "for ループの更新句がカウンタを正しい方向に進めることを強制する"
},
{
"id": "eslint/getter-return",
"description": "Enforce return statements in getters"
"description": "Enforce return statements in getters",
"descriptionZh": "强制 getter 中有 return 语句",
"descriptionJa": "getter に return 文を強制する"
},
{
"id": "eslint/no-async-promise-executor",
"description": "Disallow using an async function as a Promise executor"
"description": "Disallow using an async function as a Promise executor",
"descriptionZh": "禁止使用 async 函数作为 Promise 执行器",
"descriptionJa": "async 関数を Promise のエグゼキュータとして使用しない"
},
{
"id": "eslint/no-case-declarations",
"description": "Disallow lexical declarations in case clauses"
"description": "Disallow lexical declarations in case clauses",
"descriptionZh": "禁止在 case 子句中声明词法变量",
"descriptionJa": "case 節での語彙宣言を禁止する"
},
{
"id": "eslint/no-class-assign",
"description": "Disallow reassigning class members"
"description": "Disallow reassigning class members",
"descriptionZh": "禁止重新赋值类成员",
"descriptionJa": "クラスメンバーへの再代入を禁止する"
},
{
"id": "eslint/no-compare-neg-zero",
"description": "Disallow comparing against -0"
"description": "Disallow comparing against -0",
"descriptionZh": "禁止与 -0 进行比较",
"descriptionJa": "-0 との比較を禁止する"
},
{
"id": "eslint/no-cond-assign",
"description": "Disallow assignment operators in conditional expressions"
"description": "Disallow assignment operators in conditional expressions",
"descriptionZh": "禁止在条件表达式中使用赋值运算符",
"descriptionJa": "条件式での代入演算子を禁止する"
},
{
"id": "eslint/no-const-assign",
"description": "Disallow reassigning const variables"
"description": "Disallow reassigning const variables",
"descriptionZh": "禁止重新赋值 const 变量",
"descriptionJa": "const 変数への再代入を禁止する"
},
{
"id": "eslint/no-constant-binary-expression",
"description": "Disallow constant binary expressions"
"description": "Disallow constant binary expressions",
"descriptionZh": "禁止常量二元表达式",
"descriptionJa": "定数の二項式を禁止する"
},
{
"id": "eslint/no-constant-condition",
"description": "Disallow constant expressions in conditions"
"description": "Disallow constant expressions in conditions",
"descriptionZh": "禁止在条件中使用常量表达式",
"descriptionJa": "条件での定数式を禁止する"
},
{
"id": "eslint/no-control-regex",
"description": "Disallow control characters in regular expressions"
"description": "Disallow control characters in regular expressions",
"descriptionZh": "禁止正则表达式中的控制字符",
"descriptionJa": "正規表現内の制御文字を禁止する"
},
{
"id": "eslint/no-debugger",
"description": "Disallow the use of debugger"
"description": "Disallow the use of debugger",
"descriptionZh": "禁止使用 debugger 语句",
"descriptionJa": "debugger 文の使用を禁止する"
},
{
"id": "eslint/no-delete-var",
"description": "Disallow deleting variables"
"description": "Disallow deleting variables",
"descriptionZh": "禁止删除变量",
"descriptionJa": "変数の削除を禁止する"
},
{
"id": "eslint/no-dupe-args",
"description": "Disallow duplicate arguments in function definitions"
"description": "Disallow duplicate arguments in function definitions",
"descriptionZh": "禁止函数定义中重复的参数",
"descriptionJa": "関数定義内の重複引数を禁止する"
},
{
"id": "eslint/no-dupe-class-members",
"description": "Disallow duplicate class members"
"description": "Disallow duplicate class members",
"descriptionZh": "禁止重复的类成员",
"descriptionJa": "重複するクラスメンバーを禁止する"
},
{
"id": "eslint/no-dupe-else-if",
"description": "Disallow duplicate conditions in if-else-if chains"
"description": "Disallow duplicate conditions in if-else-if chains",
"descriptionZh": "禁止 if-else-if 链中的重复条件",
"descriptionJa": "if-else-if チェーン内の重複条件を禁止する"
},
{
"id": "eslint/no-dupe-keys",
"description": "Disallow duplicate keys in object literals"
"description": "Disallow duplicate keys in object literals",
"descriptionZh": "禁止对象字面量中重复的键",
"descriptionJa": "オブジェクトリテラル内の重複キーを禁止する"
},
{
"id": "eslint/no-duplicate-case",
"description": "Disallow duplicate case labels"
"description": "Disallow duplicate case labels",
"descriptionZh": "禁止重复的 case 标签",
"descriptionJa": "重複する case ラベルを禁止する"
},
{
"id": "eslint/no-empty",
"description": "Disallow empty block statements"
"description": "Disallow empty block statements",
"descriptionZh": "禁止空块语句",
"descriptionJa": "空のブロック文を禁止する"
},
{
"id": "eslint/no-empty-character-class",
"description": "Disallow empty character classes in regular expressions"
"description": "Disallow empty character classes in regular expressions",
"descriptionZh": "禁止正则表达式中的空字符类",
"descriptionJa": "正規表現内の空の文字クラスを禁止する"
},
{
"id": "eslint/no-empty-pattern",
"description": "Disallow empty destructuring patterns"
"description": "Disallow empty destructuring patterns",
"descriptionZh": "禁止空解构模式",
"descriptionJa": "空の分割代入パターンを禁止する"
},
{
"id": "eslint/no-empty-static-block",
"description": "Disallow empty static blocks"
"description": "Disallow empty static blocks",
"descriptionZh": "禁止空的静态块",
"descriptionJa": "空の静的ブロックを禁止する"
},
{
"id": "eslint/no-ex-assign",
"description": "Disallow reassigning exceptions in catch clauses"
"description": "Disallow reassigning exceptions in catch clauses",
"descriptionZh": "禁止在 catch 子句中重新赋值异常",
"descriptionJa": "catch 句での例外への再代入を禁止する"
},
{
"id": "eslint/no-extra-boolean-cast",
"description": "Disallow unnecessary boolean casts"
"description": "Disallow unnecessary boolean casts",
"descriptionZh": "禁止不必要的布尔转换",
"descriptionJa": "不要なブール変換を禁止する"
},
{
"id": "eslint/no-fallthrough",
"description": "Disallow fallthrough of case statements"
"description": "Disallow fallthrough of case statements",
"descriptionZh": "禁止 case 语句的 fallthrough",
"descriptionJa": "case 文のフォールスルーを禁止する"
},
{
"id": "eslint/no-func-assign",
"description": "Disallow reassigning function declarations"
"description": "Disallow reassigning function declarations",
"descriptionZh": "禁止重新赋值函数声明",
"descriptionJa": "関数宣言への再代入を禁止する"
},
{
"id": "eslint/no-global-assign",
"description": "Disallow assignments to native objects or read-only global variables"
"description": "Disallow assignments to native objects or read-only global variables",
"descriptionZh": "禁止对原生对象或只读全局变量赋值",
"descriptionJa": "ネイティブオブジェクトや読み取り専用グローバルへの代入を禁止する"
},
{
"id": "eslint/no-import-assign",
"description": "Disallow assigning to imported bindings"
"description": "Disallow assigning to imported bindings",
"descriptionZh": "禁止对导入的绑定赋值",
"descriptionJa": "インポートされたバインディングへの代入を禁止する"
},
{
"id": "eslint/no-invalid-regexp",
"description": "Disallow invalid regular expression strings in RegExp constructors"
"description": "Disallow invalid regular expression strings in RegExp constructors",
"descriptionZh": "禁止 RegExp 构造函数中的无效正则字符串",
"descriptionJa": "RegExp コンストラクタ内の不正な正規表現文字列を禁止する"
},
{
"id": "eslint/no-irregular-whitespace",
"description": "Disallow irregular whitespace"
"description": "Disallow irregular whitespace",
"descriptionZh": "禁止不规则空白",
"descriptionJa": "不規則な空白を禁止する"
},
{
"id": "eslint/no-loss-of-precision",
"description": "Disallow literal numbers that lose precision"
"description": "Disallow literal numbers that lose precision",
"descriptionZh": "禁止会丢失精度的字面数字",
"descriptionJa": "精度を失うリテラル数値を禁止する"
},
{
"id": "eslint/no-misleading-character-class",
"description": "Disallow characters which are made with multiple code points in character class syntax"
"description": "Disallow characters which are made with multiple code points in character class syntax",
"descriptionZh": "禁止字符类语法中使用多个码点构成的字符",
"descriptionJa": "文字クラス構文で複数のコードポイントからなる文字を禁止する"
},
{
"id": "eslint/no-new-native-nonconstructor",
"description": "Disallow new operators with global non-constructor functions"
"description": "Disallow new operators with global non-constructor functions",
"descriptionZh": "禁止对全局非构造函数使用 new",
"descriptionJa": "グローバルな非コンストラクタ関数への new を禁止する"
},
{
"id": "eslint/no-nonoctal-decimal-escape",
"description": "Disallow \\8 and \\9 escape sequences in string literals"
"description": "Disallow \\8 and \\9 escape sequences in string literals",
"descriptionZh": "禁止字符串字面量中的 \\8 和 \\9 转义序列",
"descriptionJa": "文字列リテラル内の \\8 と \\9 のエスケープシーケンスを禁止する"
},
{
"id": "eslint/no-obj-calls",
"description": "Disallow calling global object properties as functions"
"description": "Disallow calling global object properties as functions",
"descriptionZh": "禁止将全局对象属性作为函数调用",
"descriptionJa": "グローバルオブジェクトのプロパティを関数として呼び出すことを禁止する"
},
{
"id": "eslint/no-octal",
"description": "Disallow octal literals"
"description": "Disallow octal literals",
"descriptionZh": "禁止八进制字面量",
"descriptionJa": "8進数リテラルを禁止する"
},
{
"id": "eslint/no-prototype-builtins",
"description": "Disallow calling some Object.prototype methods directly on objects"
"description": "Disallow calling some Object.prototype methods directly on objects",
"descriptionZh": "禁止直接在对象上调用某些 Object.prototype 方法",
"descriptionJa": "オブジェクトで Object.prototype の一部メソッドを直接呼ぶことを禁止する"
},
{
"id": "eslint/no-redeclare",
"description": "Disallow variable redeclaration"
"description": "Disallow variable redeclaration",
"descriptionZh": "禁止变量重新声明",
"descriptionJa": "変数の再宣言を禁止する"
},
{
"id": "eslint/no-regex-spaces",
"description": "Disallow multiple spaces in regular expression literals"
"description": "Disallow multiple spaces in regular expression literals",
"descriptionZh": "禁止正则表达式字面量中的多个空格",
"descriptionJa": "正規表現リテラル内の複数スペースを禁止する"
},
{
"id": "eslint/no-self-assign",
"description": "Disallow assignments where both sides are exactly the same"
"description": "Disallow assignments where both sides are exactly the same",
"descriptionZh": "禁止两侧完全相同的赋值",
"descriptionJa": "両辺が完全に同一の代入を禁止する"
},
{
"id": "eslint/no-setter-return",
"description": "Disallow returning values from setters"
"description": "Disallow returning values from setters",
"descriptionZh": "禁止 setter 返回值",
"descriptionJa": "setter からの戻り値を禁止する"
},
{
"id": "eslint/no-shadow-restricted-names",
"description": "Disallow identifiers from shadowing restricted names"
"description": "Disallow identifiers from shadowing restricted names",
"descriptionZh": "禁止标识符遮蔽受限名称",
"descriptionJa": "予約名を遮蔽する識別子を禁止する"
},
{
"id": "eslint/no-sparse-arrays",
"description": "Disallow sparse arrays"
"description": "Disallow sparse arrays",
"descriptionZh": "禁止稀疏数组",
"descriptionJa": "疎配列を禁止する"
},
{
"id": "eslint/no-this-before-super",
"description": "Disallow this/super before calling super() in constructors"
"description": "Disallow this/super before calling super() in constructors",
"descriptionZh": "禁止在构造函数中调用 super() 之前使用 this/super",
"descriptionJa": "コンストラクタで super() 呼び出し前の this/super 使用を禁止する"
},
{
"id": "eslint/no-undef",
"description": "Disallow undeclared variables"
"description": "Disallow undeclared variables",
"descriptionZh": "禁止使用未声明的变量",
"descriptionJa": "未宣言の変数の使用を禁止する"
},
{
"id": "eslint/no-unexpected-multiline",
"description": "Disallow confusing multiline expressions"
"description": "Disallow confusing multiline expressions",
"descriptionZh": "禁止令人困惑的多行表达式",
"descriptionJa": "紛らわしい複数行式を禁止する"
},
{
"id": "eslint/no-unreachable",
"description": "Disallow unreachable code after return, throw, continue, and break statements"
"description": "Disallow unreachable code after return, throw, continue, and break statements",
"descriptionZh": "禁止 return/throw/continue/break 之后不可达的代码",
"descriptionJa": "return/throw/continue/break 後の到達不能コードを禁止する"
},
{
"id": "eslint/no-unsafe-finally",
"description": "Disallow control flow statements in finally blocks"
"description": "Disallow control flow statements in finally blocks",
"descriptionZh": "禁止 finally 块中的控制流语句",
"descriptionJa": "finally ブロック内の制御フロー文を禁止する"
},
{
"id": "eslint/no-unsafe-negation",
"description": "Disallow negating the left operand of relational operators"
"description": "Disallow negating the left operand of relational operators",
"descriptionZh": "禁止对关系运算符左操作数取反",
"descriptionJa": "関係演算子の左オペランドの否定を禁止する"
},
{
"id": "eslint/no-unsafe-optional-chaining",
"description": "Disallow use of optional chaining in contexts where undefined is not allowed"
"description": "Disallow use of optional chaining in contexts where undefined is not allowed",
"descriptionZh": "禁止在 undefined 不允许的上下文中使用可选链",
"descriptionJa": "undefined が許されない文脈でのオプショナルチェーンを禁止する"
},
{
"id": "eslint/no-unused-labels",
"description": "Disallow unused labels"
"description": "Disallow unused labels",
"descriptionZh": "禁止未使用的标签",
"descriptionJa": "未使用のラベルを禁止する"
},
{
"id": "eslint/no-unused-private-class-members",
"description": "Disallow unused private class members"
"description": "Disallow unused private class members",
"descriptionZh": "禁止未使用的私有类成员",
"descriptionJa": "未使用のプライベートクラスメンバーを禁止する"
},
{
"id": "eslint/no-unused-vars",
"description": "Disallow unused variables"
"description": "Disallow unused variables",
"descriptionZh": "禁止未使用的变量",
"descriptionJa": "未使用の変数を禁止する"
},
{
"id": "eslint/no-useless-backreference",
"description": "Disallow useless backreferences in regular expressions"
"description": "Disallow useless backreferences in regular expressions",
"descriptionZh": "禁止正则表达式中无用的反向引用",
"descriptionJa": "正規表現内の無用な後方参照を禁止する"
},
{
"id": "eslint/no-useless-catch",
"description": "Disallow unnecessary catch clauses"
"description": "Disallow unnecessary catch clauses",
"descriptionZh": "禁止不必要的 catch 子句",
"descriptionJa": "不要な catch 句を禁止する"
},
{
"id": "eslint/no-useless-escape",
"description": "Disallow unnecessary escape characters"
"description": "Disallow unnecessary escape characters",
"descriptionZh": "禁止不必要的转义字符",
"descriptionJa": "不要なエスケープ文字を禁止する"
},
{
"id": "eslint/no-with",
"description": "Disallow with statements"
"description": "Disallow with statements",
"descriptionZh": "禁止 with 语句",
"descriptionJa": "with 文を禁止する"
},
{
"id": "eslint/require-yield",
"description": "Require generator functions to contain yield"
"description": "Require generator functions to contain yield",
"descriptionZh": "要求生成器函数包含 yield",
"descriptionJa": "ジェネレータ関数に yield を含めることを要求する"
},
{
"id": "eslint/use-isnan",
"description": "Require calls to isNaN() when checking for NaN"
"description": "Require calls to isNaN() when checking for NaN",
"descriptionZh": "检查 NaN 时要求调用 isNaN()",
"descriptionJa": "NaN のチェック時に isNaN() の呼び出しを要求する"
},
{
"id": "eslint/valid-typeof",
"description": "Enforce comparing typeof expressions against valid strings"
"description": "Enforce comparing typeof expressions against valid strings",
"descriptionZh": "强制 typeof 表达式与有效字符串比较",
"descriptionJa": "typeof 式と有効な文字列の比較を強制する"
},
{"id": "eslint/eqeqeq", "description": "Require === and !=="},
{"id": "eslint/no-eq-null", "description": "Disallow null comparisons without type-checking"},
{"id": "eslint/no-self-compare", "description": "Disallow comparisons where both sides are the same"},
{"id": "eslint/no-await-in-loop", "description": "Disallow await inside loops"},
{"id": "eslint/no-promise-executor-return", "description": "Disallow returning values from Promise executor"},
{"id": "eslint/no-shadow", "description": "Disallow variable declarations from shadowing variables in outer scopes"},
{"id": "eslint/no-unassigned-vars", "description": "Disallow let or var variables that are read but never assigned"},
{"id": "eslint/no-useless-assignment", "description": "Disallow variable assignments where the value is not used"},
{"id": "eslint/block-scoped-var", "description": "Enforce variables within the scope they are defined"},
{"id": "eslint/default-case", "description": "Require default cases in switch statements"},
{"id": "eslint/default-case-last", "description": "Enforce default clauses in switch statements to be last"},
{"id": "eslint/no-unmodified-loop-condition", "description": "Disallow unmodified loop conditions"},
{"id": "eslint/no-unreachable-loop", "description": "Disallow loops with a body that allows only one iteration"},
{"id": "eslint/no-eval", "description": "Disallow the use of eval()"},
{"id": "eslint/no-extend-native", "description": "Disallow extending native types"},
{"id": "eslint/no-var", "description": "Require let or const instead of var"},
{"id": "eslint/prefer-template", "description": "Require template literals instead of string concatenation"},
{"id": "eslint/prefer-object-spread", "description": "Disallow Object.assign and prefer object spread"},
{"id": "eslint/prefer-rest-params", "description": "Require rest parameters instead of arguments"},
{"id": "eslint/prefer-spread", "description": "Require spread operator instead of .apply()"},
{"id": "eslint/prefer-object-has-own", "description": "Disallow Object.prototype.hasOwnProperty.call() and prefer Object.hasOwn()"},
{"id": "eslint/no-useless-concat", "description": "Disallow unnecessary concatenation of literals or template literals"},
{"id": "eslint/no-useless-return", "description": "Disallow redundant return statements"},
{"id": "eslint/no-useless-computed-key", "description": "Disallow unnecessary computed property keys in objects and classes"},
{"id": "eslint/no-useless-rename", "description": "Disallow renaming import, export, and destructured assignments to the same name"},
{"id": "eslint/no-param-reassign", "description": "Disallow reassigning function parameters"},
{"id": "eslint/no-return-assign", "description": "Disallow assignment operators in return statements"},
{"id": "eslint/no-throw-literal", "description": "Disallow throwing literals as exceptions"},
{"id": "eslint/camelcase", "description": "Enforce camelcase naming convention"},
{"id": "eslint/new-cap", "description": "Require constructor names to begin with a capital letter"},
{"id": "eslint/no-array-constructor", "description": "Disallow Array constructors"}
{
"id": "eslint/eqeqeq",
"description": "Require === and !==",
"descriptionZh": "要求使用 === 和 !==",
"descriptionJa": "=== と !== の使用を要求する"
},
{
"id": "eslint/no-eq-null",
"description": "Disallow null comparisons without type-checking",
"descriptionZh": "禁止无类型检查的 null 比较",
"descriptionJa": "型チェックなしの null 比較を禁止する"
},
{
"id": "eslint/no-self-compare",
"description": "Disallow comparisons where both sides are the same",
"descriptionZh": "禁止两侧相同的比较",
"descriptionJa": "両辺が同一の比較を禁止する"
},
{
"id": "eslint/no-await-in-loop",
"description": "Disallow await inside loops",
"descriptionZh": "禁止在循环内使用 await",
"descriptionJa": "ループ内での await を禁止する"
},
{
"id": "eslint/no-promise-executor-return",
"description": "Disallow returning values from Promise executor",
"descriptionZh": "禁止 Promise 执行器返回值",
"descriptionJa": "Promise エグゼキュータからの戻り値を禁止する"
},
{
"id": "eslint/no-shadow",
"description": "Disallow variable declarations from shadowing variables in outer scopes",
"descriptionZh": "禁止变量声明遮蔽外层作用域中的变量",
"descriptionJa": "外側スコープの変数を遮蔽する宣言を禁止する"
},
{
"id": "eslint/no-unassigned-vars",
"description": "Disallow let or var variables that are read but never assigned",
"descriptionZh": "禁止只读但从未赋值的 let/var 变量",
"descriptionJa": "読み取られるが代入されない let/var 変数を禁止する"
},
{
"id": "eslint/no-useless-assignment",
"description": "Disallow variable assignments where the value is not used",
"descriptionZh": "禁止值未被使用的变量赋值",
"descriptionJa": "値が使われない変数への代入を禁止する"
},
{
"id": "eslint/block-scoped-var",
"description": "Enforce variables within the scope they are defined",
"descriptionZh": "强制变量在其定义的作用域内使用",
"descriptionJa": "変数を定義されたスコープ内で使用することを強制する"
},
{
"id": "eslint/default-case",
"description": "Require default cases in switch statements",
"descriptionZh": "要求 switch 语句有 default 子句",
"descriptionJa": "switch 文に default 句を要求する"
},
{
"id": "eslint/default-case-last",
"description": "Enforce default clauses in switch statements to be last",
"descriptionZh": "强制 switch 语句中 default 子句在最后",
"descriptionJa": "switch 文で default 句を最後にすることを強制する"
},
{
"id": "eslint/no-unmodified-loop-condition",
"description": "Disallow unmodified loop conditions",
"descriptionZh": "禁止未修改的循环条件",
"descriptionJa": "変更されないループ条件を禁止する"
},
{
"id": "eslint/no-unreachable-loop",
"description": "Disallow loops with a body that allows only one iteration",
"descriptionZh": "禁止只允许一次迭代的循环体",
"descriptionJa": "一度しか反復できないループを禁止する"
},
{
"id": "eslint/no-eval",
"description": "Disallow the use of eval()",
"descriptionZh": "禁止使用 eval()",
"descriptionJa": "eval() の使用を禁止する"
},
{
"id": "eslint/no-extend-native",
"description": "Disallow extending native types",
"descriptionZh": "禁止扩展原生类型",
"descriptionJa": "ネイティブ型の拡張を禁止する"
},
{
"id": "eslint/no-var",
"description": "Require let or const instead of var",
"descriptionZh": "要求使用 let 或 const 替代 var",
"descriptionJa": "var の代わりに let または const を要求する"
},
{
"id": "eslint/prefer-template",
"description": "Require template literals instead of string concatenation",
"descriptionZh": "要求使用模板字面量替代字符串拼接",
"descriptionJa": "文字列連結の代わりにテンプレートリテラルを要求する"
},
{
"id": "eslint/prefer-object-spread",
"description": "Disallow Object.assign and prefer object spread",
"descriptionZh": "禁止 Object.assign,优先使用对象展开",
"descriptionJa": "Object.assign を禁止しオブジェクト展開を推奨する"
},
{
"id": "eslint/prefer-rest-params",
"description": "Require rest parameters instead of arguments",
"descriptionZh": "要求使用剩余参数替代 arguments",
"descriptionJa": "arguments の代わりに残余引数を要求する"
},
{
"id": "eslint/prefer-spread",
"description": "Require spread operator instead of .apply()",
"descriptionZh": "要求使用展开运算符替代 .apply()",
"descriptionJa": ".apply() の代わりにスプレッド演算子を要求する"
},
{
"id": "eslint/prefer-object-has-own",
"description": "Disallow Object.prototype.hasOwnProperty.call() and prefer Object.hasOwn()",
"descriptionZh": "禁止 Object.prototype.hasOwnProperty.call(),优先使用 Object.hasOwn()",
"descriptionJa": "Object.prototype.hasOwnProperty.call() を禁止し Object.hasOwn() を推奨する"
},
{
"id": "eslint/no-useless-concat",
"description": "Disallow unnecessary concatenation of literals or template literals",
"descriptionZh": "禁止不必要的字面量或模板字面量拼接",
"descriptionJa": "不要なリテラルやテンプレートリテラルの連結を禁止する"
},
{
"id": "eslint/no-useless-return",
"description": "Disallow redundant return statements",
"descriptionZh": "禁止冗余的 return 语句",
"descriptionJa": "冗長な return 文を禁止する"
},
{
"id": "eslint/no-useless-computed-key",
"description": "Disallow unnecessary computed property keys in objects and classes",
"descriptionZh": "禁止对象和类中不必要的计算属性键",
"descriptionJa": "オブジェクトとクラス内の不要な算出プロパティキーを禁止する"
},
{
"id": "eslint/no-useless-rename",
"description": "Disallow renaming import, export, and destructured assignments to the same name",
"descriptionZh": "禁止将导入、导出和解构赋值重命名为相同名称",
"descriptionJa": "インポート・エクスポート・分割代入を同名に改名することを禁止する"
},
{
"id": "eslint/no-param-reassign",
"description": "Disallow reassigning function parameters",
"descriptionZh": "禁止重新赋值函数参数",
"descriptionJa": "関数パラメータへの再代入を禁止する"
},
{
"id": "eslint/no-return-assign",
"description": "Disallow assignment operators in return statements",
"descriptionZh": "禁止在 return 语句中使用赋值运算符",
"descriptionJa": "return 文での代入演算子を禁止する"
},
{
"id": "eslint/no-throw-literal",
"description": "Disallow throwing literals as exceptions",
"descriptionZh": "禁止将字面量作为异常抛出",
"descriptionJa": "リテラルを例外として投げることを禁止する"
},
{
"id": "eslint/camelcase",
"description": "Enforce camelcase naming convention",
"descriptionZh": "强制使用 camelCase 命名规范",
"descriptionJa": "camelCase 命名規則を強制する"
},
{
"id": "eslint/new-cap",
"description": "Require constructor names to begin with a capital letter",
"descriptionZh": "要求构造函数名以大写字母开头",
"descriptionJa": "コンストラクタ名を大文字で始めることを要求する"
},
{
"id": "eslint/no-array-constructor",
"description": "Disallow Array constructors",
"descriptionZh": "禁止使用 Array 构造函数",
"descriptionJa": "Array コンストラクタを禁止する"
}
],
"ts-eslint": [
{
"id": "ts-eslint/ban-ts-comment",
"description": "Disallow @ts-<directive> comments"
"description": "Disallow @ts-<directive> comments",
"descriptionZh": "禁止使用 @ts-<指令> 注释",
"descriptionJa": "@ts-<ディレクティブ> コメントを禁止する"
},
{
"id": "ts-eslint/no-array-constructor",
"description": "Disallow generic Array constructors"
"description": "Disallow generic Array constructors",
"descriptionZh": "禁止泛型 Array 构造函数",
"descriptionJa": "ジェネリック Array コンストラクタを禁止する"
},
{
"id": "ts-eslint/no-duplicate-enum-values",
"description": "Disallow duplicate enum member values"
"description": "Disallow duplicate enum member values",
"descriptionZh": "禁止重复的枚举成员值",
"descriptionJa": "重複する列挙メンバー値を禁止する"
},
{
"id": "ts-eslint/no-empty-object-type",
"description": "Disallow empty object types"
"description": "Disallow empty object types",
"descriptionZh": "禁止空对象类型",
"descriptionJa": "空のオブジェクト型を禁止する"
},
{
"id": "ts-eslint/no-explicit-any",
"description": "Disallow the any type"
"description": "Disallow the any type",
"descriptionZh": "禁止使用 any 类型",
"descriptionJa": "any 型の使用を禁止する"
},
{
"id": "ts-eslint/no-extra-non-null-assertion",
"description": "Disallow extra non-null assertions"
"description": "Disallow extra non-null assertions",
"descriptionZh": "禁止多余的非空断言",
"descriptionJa": "余分な非nullアサーションを禁止する"
},
{
"id": "ts-eslint/no-misused-new",
"description": "Enforce valid definition of new and constructor"
"description": "Enforce valid definition of new and constructor",
"descriptionZh": "强制 new 和 constructor 的有效定义",
"descriptionJa": "new とコンストラクタの有効な定義を強制する"
},
{
"id": "ts-eslint/no-namespace",
"description": "Disallow custom TypeScript modules and namespaces"
"description": "Disallow custom TypeScript modules and namespaces",
"descriptionZh": "禁止自定义 TypeScript 模块和命名空间",
"descriptionJa": "カスタム TypeScript モジュールと名前空間を禁止する"
},
{
"id": "ts-eslint/no-non-null-asserted-optional-chain",
"description": "Disallow non-null assertions after optional chain"
"description": "Disallow non-null assertions after optional chain",
"descriptionZh": "禁止可选链之后的非空断言",
"descriptionJa": "オプショナルチェーン後の非nullアサーションを禁止する"
},
{
"id": "ts-eslint/no-require-imports",
"description": "Disallow invocation of require()"
"description": "Disallow invocation of require()",
"descriptionZh": "禁止调用 require()",
"descriptionJa": "require() の呼び出しを禁止する"
},
{
"id": "ts-eslint/no-this-alias",
"description": "Disallow aliasing this"
"description": "Disallow aliasing this",
"descriptionZh": "禁止为 this 创建别名",
"descriptionJa": "this のエイリアス作成を禁止する"
},
{
"id": "ts-eslint/no-unnecessary-type-constraint",
"description": "Disallow unnecessary constraints on generic types"
"description": "Disallow unnecessary constraints on generic types",
"descriptionZh": "禁止泛型类型上不必要的约束",
"descriptionJa": "ジェネリック型の不要な制約を禁止する"
},
{
"id": "ts-eslint/no-unsafe-declaration-merging",
"description": "Disallow unsafe declaration merging"
"description": "Disallow unsafe declaration merging",
"descriptionZh": "禁止不安全的声明合并",
"descriptionJa": "安全でない宣言のマージを禁止する"
},
{
"id": "ts-eslint/no-unsafe-function-type",
"description": "Disallow using Function as a type"
"description": "Disallow using Function as a type",
"descriptionZh": "禁止使用 Function 作为类型",
"descriptionJa": "Function を型として使用することを禁止する"
},
{
"id": "ts-eslint/no-unused-expressions",
"description": "Disallow unused expressions"
"description": "Disallow unused expressions",
"descriptionZh": "禁止未使用的表达式",
"descriptionJa": "未使用の式を禁止する"
},
{
"id": "ts-eslint/no-unused-vars",
"description": "Disallow unused variables"
"description": "Disallow unused variables",
"descriptionZh": "禁止未使用的变量",
"descriptionJa": "未使用の変数を禁止する"
},
{
"id": "ts-eslint/no-wrapper-object-types",
"description": "Disallow wrapper object types (String, Number, Boolean)"
"description": "Disallow wrapper object types (String, Number, Boolean)",
"descriptionZh": "禁止包装对象类型(String、Number、Boolean",
"descriptionJa": "ラッパーオブジェクト型(String、Number、Boolean)を禁止する"
},
{
"id": "ts-eslint/prefer-as-const",
"description": "Prefer as const over literal type annotation"
"description": "Prefer as const over literal type annotation",
"descriptionZh": "优先使用 as const 而非字面量类型注解",
"descriptionJa": "リテラル型注釈より as const を推奨する"
},
{
"id": "ts-eslint/prefer-namespace-keyword",
"description": "Require using namespace keyword over module keyword"
"description": "Require using namespace keyword over module keyword",
"descriptionZh": "要求使用 namespace 关键字替代 module",
"descriptionJa": "module キーワードより namespace の使用を要求する"
},
{
"id": "ts-eslint/triple-slash-reference",
"description": "Disallow certain triple slash directives"
"description": "Disallow certain triple slash directives",
"descriptionZh": "禁止某些三斜线指令",
"descriptionJa": "特定のトリプルスラッシュディレクティブを禁止する"
},
{
"id": "ts-eslint/no-var",
"description": "Require let or const instead of var"
"description": "Require let or const instead of var",
"descriptionZh": "要求使用 let 或 const 替代 var",
"descriptionJa": "var の代わりに let または const を要求する"
},
{
"id": "ts-eslint/prefer-const",
"description": "Require const declarations for never-reassigned variables"
"description": "Require const declarations for never-reassigned variables",
"descriptionZh": "对从未重新赋值的变量要求使用 const",
"descriptionJa": "再代入されない変数に const を要求する"
},
{
"id": "ts-eslint/prefer-rest-params",
"description": "Require rest parameters instead of arguments"
"description": "Require rest parameters instead of arguments",
"descriptionZh": "要求使用剩余参数替代 arguments",
"descriptionJa": "arguments の代わりに残余引数を要求する"
},
{
"id": "ts-eslint/prefer-spread",
"description": "Require spread operator instead of .apply()"
"description": "Require spread operator instead of .apply()",
"descriptionZh": "要求使用展开运算符替代 .apply()",
"descriptionJa": ".apply() の代わりにスプレッド演算子を要求する"
},
{"id": "ts-eslint/no-non-null-assertion", "description": "Disallow non-null assertions using the ! postfix operator"},
{"id": "ts-eslint/no-dynamic-delete", "description": "Disallow using the delete operator on computed key expressions"},
{"id": "ts-eslint/no-useless-empty-export", "description": "Disallow empty exports that don't change anything in a module"},
{"id": "ts-eslint/consistent-type-imports", "description": "Enforce consistent usage of type imports"},
{"id": "ts-eslint/unified-signatures", "description": "Disallow two overloads that could be unified into a single signature"},
{"id": "ts-eslint/no-extraneous-class", "description": "Disallow classes only being used as namespaces"},
{"id": "ts-eslint/no-useless-constructor", "description": "Disallow unnecessary constructors"},
{"id": "ts-eslint/no-non-null-asserted-nullish-coalescing", "description": "Disallow non-null assertions in the left operand of a nullish coalescing operator"},
{"id": "ts-eslint/no-invalid-void-type", "description": "Disallow void type outside of generic or return types"},
{"id": "ts-eslint/prefer-literal-enum-member", "description": "Require all enum members to be literal values"},
{"id": "ts-eslint/prefer-enum-initializers", "description": "Require each enum member value to be explicitly initialized"},
{"id": "ts-eslint/no-shadow", "description": "Disallow variable declarations from shadowing variables declared in the outer scope"}
{
"id": "ts-eslint/no-non-null-assertion",
"description": "Disallow non-null assertions using the ! postfix operator",
"descriptionZh": "禁止使用 ! 后缀运算符进行非空断言",
"descriptionJa": "! 接尾辞演算子による非nullアサーションを禁止する"
},
{
"id": "ts-eslint/no-dynamic-delete",
"description": "Disallow using the delete operator on computed key expressions",
"descriptionZh": "禁止对计算键表达式使用 delete 运算符",
"descriptionJa": "算出キー式への delete 演算子の使用を禁止する"
},
{
"id": "ts-eslint/no-useless-empty-export",
"description": "Disallow empty exports that don't change anything in a module",
"descriptionZh": "禁止不改变模块内容的空导出",
"descriptionJa": "モジュールに変更を加えない空のエクスポートを禁止する"
},
{
"id": "ts-eslint/consistent-type-imports",
"description": "Enforce consistent usage of type imports",
"descriptionZh": "强制类型导入的一致用法",
"descriptionJa": "型インポートの一貫した使用を強制する"
},
{
"id": "ts-eslint/unified-signatures",
"description": "Disallow two overloads that could be unified into a single signature",
"descriptionZh": "禁止可合并为单一签名的两个重载",
"descriptionJa": "単一のシグネチャに統合できる2つのオーバーロードを禁止する"
},
{
"id": "ts-eslint/no-extraneous-class",
"description": "Disallow classes only being used as namespaces",
"descriptionZh": "禁止仅用作命名空间的类",
"descriptionJa": "名前空間としてのみ使用されるクラスを禁止する"
},
{
"id": "ts-eslint/no-useless-constructor",
"description": "Disallow unnecessary constructors",
"descriptionZh": "禁止不必要的构造函数",
"descriptionJa": "不要なコンストラクタを禁止する"
},
{
"id": "ts-eslint/no-non-null-asserted-nullish-coalescing",
"description": "Disallow non-null assertions in the left operand of a nullish coalescing operator",
"descriptionZh": "禁止空值合并运算符左操作数中的非空断言",
"descriptionJa": "null合体演算子の左オペランドでの非nullアサーションを禁止する"
},
{
"id": "ts-eslint/no-invalid-void-type",
"description": "Disallow void type outside of generic or return types",
"descriptionZh": "禁止泛型或返回类型之外的 void 类型",
"descriptionJa": "ジェネリックまたは戻り値型以外での void 型を禁止する"
},
{
"id": "ts-eslint/prefer-literal-enum-member",
"description": "Require all enum members to be literal values",
"descriptionZh": "要求所有枚举成员为字面量值",
"descriptionJa": "すべての列挙メンバーにリテラル値を要求する"
},
{
"id": "ts-eslint/prefer-enum-initializers",
"description": "Require each enum member value to be explicitly initialized",
"descriptionZh": "要求每个枚举成员值被显式初始化",
"descriptionJa": "各列挙メンバー値の明示的な初期化を要求する"
},
{
"id": "ts-eslint/no-shadow",
"description": "Disallow variable declarations from shadowing variables declared in the outer scope",
"descriptionZh": "禁止变量声明遮蔽外层作用域中声明的变量",
"descriptionJa": "外側スコープで宣言された変数を遮蔽する宣言を禁止する"
}
],
"stylelint": [
{
"id": "stylelint/color-hex-length",
"description": "Specify short or long hexadecimal color values"
"description": "Specify short or long hexadecimal color values",
"descriptionZh": "指定十六进制颜色值的短或长格式",
"descriptionJa": "16進カラー値の短い形式または長い形式を指定する"
},
{
"id": "stylelint/color-named",
"description": "Require (where possible) or disallow named colors"
"description": "Require (where possible) or disallow named colors",
"descriptionZh": "要求(尽可能)或禁止使用命名颜色",
"descriptionJa": "名前付きカラーの使用を(可能な限り)要求または禁止する"
},
{
"id": "stylelint/color-no-invalid-hex",
"description": "Disallow invalid hex colors"
"description": "Disallow invalid hex colors",
"descriptionZh": "禁止无效的十六进制颜色",
"descriptionJa": "無効な16進カラーを禁止する"
},
{
"id": "stylelint/length-zero-no-unit",
"description": "Disallow units for zero lengths"
"description": "Disallow units for zero lengths",
"descriptionZh": "禁止零长度带单位",
"descriptionJa": "ゼロ長に単位を付けることを禁止する"
},
{
"id": "stylelint/font-family-no-missing-generic-family-keyword",
"description": "Disallow missing generic families in font-family"
"description": "Disallow missing generic families in font-family",
"descriptionZh": "禁止 font-family 中缺少通用字体族",
"descriptionJa": "font-family での汎用ファミリーキーワードの欠落を禁止する"
},
{
"id": "stylelint/block-no-empty",
"description": "Disallow empty blocks"
"description": "Disallow empty blocks",
"descriptionZh": "禁止空块",
"descriptionJa": "空のブロックを禁止する"
},
{
"id": "stylelint/declaration-block-no-duplicate-properties",
"description": "Disallow duplicate properties within declaration blocks"
"description": "Disallow duplicate properties within declaration blocks",
"descriptionZh": "禁止声明块内重复的属性",
"descriptionJa": "宣言ブロック内の重複プロパティを禁止する"
},
{
"id": "stylelint/no-descending-specificity",
"description": "Disallow selectors of lower specificity from overriding higher specificity"
"description": "Disallow selectors of lower specificity from overriding higher specificity",
"descriptionZh": "禁止低特异性的选择器覆盖高特异性",
"descriptionJa": "低特異性のセレクタが高特異性を上書きすることを禁止する"
},
{
"id": "stylelint/unit-no-unknown",
"description": "Disallow unknown units"
"description": "Disallow unknown units",
"descriptionZh": "禁止未知单位",
"descriptionJa": "未知の単位を禁止する"
},
{
"id": "stylelint/property-no-unknown",
"description": "Disallow unknown properties"
"description": "Disallow unknown properties",
"descriptionZh": "禁止未知属性",
"descriptionJa": "未知のプロパティを禁止する"
},
{
"id": "stylelint/selector-pseudo-class-no-unknown",
"description": "Disallow unknown pseudo-class selectors"
"description": "Disallow unknown pseudo-class selectors",
"descriptionZh": "禁止未知的伪类选择器",
"descriptionJa": "未知の疑似クラスセレクタを禁止する"
},
{
"id": "stylelint/selector-pseudo-element-no-unknown",
"description": "Disallow unknown pseudo-element selectors"
"description": "Disallow unknown pseudo-element selectors",
"descriptionZh": "禁止未知的伪元素选择器",
"descriptionJa": "未知の疑似要素セレクタを禁止する"
},
{
"id": "stylelint/function-linear-gradient-no-nonstandard-direction",
"description": "Disallow non-standard directions in linear-gradient"
"description": "Disallow non-standard directions in linear-gradient",
"descriptionZh": "禁止 linear-gradient 中的非标准方向",
"descriptionJa": "linear-gradient 内の非標準方向を禁止する"
},
{
"id": "stylelint/function-no-unknown",
"description": "Disallow unknown functions"
"description": "Disallow unknown functions",
"descriptionZh": "禁止未知函数",
"descriptionJa": "未知の関数を禁止する"
},
{
"id": "stylelint/no-unknown-animations",
"description": "Disallow unknown animations"
"description": "Disallow unknown animations",
"descriptionZh": "禁止未知动画",
"descriptionJa": "未知のアニメーションを禁止する"
},
{
"id": "stylelint/no-unknown-custom-media",
"description": "Disallow unknown custom media queries"
"description": "Disallow unknown custom media queries",
"descriptionZh": "禁止未知的自定义媒体查询",
"descriptionJa": "未知のカスタムメディアクエリを禁止する"
},
{
"id": "stylelint/no-unknown-custom-properties",
"description": "Disallow unknown custom properties"
"description": "Disallow unknown custom properties",
"descriptionZh": "禁止未知的自定义属性",
"descriptionJa": "未知のカスタムプロパティを禁止する"
},
{
"id": "stylelint/at-rule-no-vendor-prefix",
"description": "Disallow vendor prefixes for at-rules"
"description": "Disallow vendor prefixes for at-rules",
"descriptionZh": "禁止 at 规则使用厂商前缀",
"descriptionJa": "atルールへのベンダープレフィックスを禁止する"
},
{
"id": "stylelint/media-feature-name-no-vendor-prefix",
"description": "Disallow vendor prefixes for media feature names"
"description": "Disallow vendor prefixes for media feature names",
"descriptionZh": "禁止媒体特性名称使用厂商前缀",
"descriptionJa": "メディア特性名へのベンダープレフィックスを禁止する"
},
{
"id": "stylelint/property-no-vendor-prefix",
"description": "Disallow vendor prefixes for properties"
"description": "Disallow vendor prefixes for properties",
"descriptionZh": "禁止属性使用厂商前缀",
"descriptionJa": "プロパティへのベンダープレフィックスを禁止する"
},
{
"id": "stylelint/selector-no-vendor-prefix",
"description": "Disallow vendor prefixes for selectors"
"description": "Disallow vendor prefixes for selectors",
"descriptionZh": "禁止选择器使用厂商前缀",
"descriptionJa": "セレクタへのベンダープレフィックスを禁止する"
},
{
"id": "stylelint/value-no-vendor-prefix",
"description": "Disallow vendor prefixes for values"
"description": "Disallow vendor prefixes for values",
"descriptionZh": "禁止值使用厂商前缀",
"descriptionJa": "値へのベンダープレフィックスを禁止する"
},
{
"id": "stylelint/color-function-notation",
"description": "Require modern or legacy notation for color-functions"
"description": "Require modern or legacy notation for color-functions",
"descriptionZh": "要求颜色函数使用现代或传统记法",
"descriptionJa": "色関数に現代または従来の記法を要求する"
},
{
"id": "stylelint/selector-pseudo-element-colon-notation",
"description": "Use single or double colon notation for pseudo-elements"
"description": "Use single or double colon notation for pseudo-elements",
"descriptionZh": "伪元素使用单冒号或双冒号记法",
"descriptionJa": "疑似要素に単コロンまたは二重コロン記法を使用する"
},
{
"id": "stylelint/import-notation",
"description": "Require string or url notation for @import"
"description": "Require string or url notation for @import",
"descriptionZh": "要求 @import 使用字符串或 url 记法",
"descriptionJa": "@import に文字列または url 記法を要求する"
},
{
"id": "stylelint/alpha-value-notation",
"description": "Require percentage or number notation for alpha-values"
"description": "Require percentage or number notation for alpha-values",
"descriptionZh": "要求透明度值使用百分比或数字记法",
"descriptionJa": "アルファ値にパーセンテージまたは数値記法を要求する"
},
{
"id": "stylelint/hue-degree-notation",
"description": "Require number or angle notation for hue degrees"
"description": "Require number or angle notation for hue degrees",
"descriptionZh": "要求色相度数使用数字或角度记法",
"descriptionJa": "色相の度に数値または角度記法を要求する"
},
{
"id": "stylelint/keyframe-selector-notation",
"description": "Require keyword or percentage notation for keyframe selectors"
"description": "Require keyword or percentage notation for keyframe selectors",
"descriptionZh": "要求关键帧选择器使用关键字或百分比记法",
"descriptionJa": "キーフレームセレクタにキーワードまたはパーセンテージ記法を要求する"
},
{
"id": "stylelint/declaration-block-no-redundant-longhand-properties",
"description": "Disallow redundant longhand properties within declaration blocks"
"description": "Disallow redundant longhand properties within declaration blocks",
"descriptionZh": "禁止声明块中冗余的 longhand 属性",
"descriptionJa": "宣言ブロック内の冗長なロングハンドプロパティを禁止する"
},
{
"id": "stylelint/shorthand-property-no-redundant-values",
"description": "Disallow redundant values within shorthand properties"
"description": "Disallow redundant values within shorthand properties",
"descriptionZh": "禁止 shorthand 属性中的冗余值",
"descriptionJa": "shorthand プロパティ内の冗長な値を禁止する"
},
{
"id": "stylelint/block-no-redundant-nested-style-rules",
"description": "Disallow redundant nested style rules within blocks"
"description": "Disallow redundant nested style rules within blocks",
"descriptionZh": "禁止块内冗余的嵌套样式规则",
"descriptionJa": "ブロック内の冗長なネストスタイルルールを禁止する"
},
{
"id": "stylelint/font-family-name-quotes",
"description": "Require quotes for font-family names"
"description": "Require quotes for font-family names",
"descriptionZh": "要求 font-family 名称使用引号",
"descriptionJa": "font-family 名に引用符を要求する"
},
{
"id": "stylelint/number-max-precision",
"description": "Limit the number of decimal places in numbers"
"description": "Limit the number of decimal places in numbers",
"descriptionZh": "限制数字的小数位数",
"descriptionJa": "数値の小数桁数を制限する"
},
{
"id": "stylelint/comment-whitespace-inside",
"description": "Require or disallow whitespace inside comments"
"description": "Require or disallow whitespace inside comments",
"descriptionZh": "要求或禁止注释内部空白",
"descriptionJa": "コメント内の空白を要求または禁止する"
}
],
"pmd": [
{
"id": "pmd/AbstractClassWithoutAbstractMethod",
"description": "Abstract class does not contain any abstract methods"
"description": "Abstract class does not contain any abstract methods",
"descriptionZh": "抽象类不包含任何抽象方法",
"descriptionJa": "抽象クラスに抽象メソッドが含まれていない"
},
{
"id": "pmd/AccessorClassGeneration",
"description": "Avoid instantiation through private constructors from outside"
"description": "Avoid instantiation through private constructors from outside",
"descriptionZh": "避免从外部通过私有构造函数实例化",
"descriptionJa": "外部からプライベートコンストラクタでインスタンス化することを避ける"
},
{
"id": "pmd/AccessorMethodGeneration",
"description": "Avoid synthetic accessor methods"
"description": "Avoid synthetic accessor methods",
"descriptionZh": "避免合成访问器方法",
"descriptionJa": "合成アクセッサメソッドを避ける"
},
{
"id": "pmd/ArrayIsStoredDirectly",
"description": "Clone objects before storing in constructors/methods"
"description": "Clone objects before storing in constructors/methods",
"descriptionZh": "存储到构造函数/方法前应克隆对象",
"descriptionJa": "コンストラクタやメソッドに格納する前にオブジェクトをクローンする"
},
{
"id": "pmd/AssertStatementInTest",
"description": "Assert statements should not be used in test code"
"description": "Assert statements should not be used in test code",
"descriptionZh": "测试代码中不应使用断言语句",
"descriptionJa": "テストコードで assert 文を使用すべきでない"
},
{
"id": "pmd/AvoidMessageDigestField",
"description": "Don't declare MessageDigest as field (thread safety)"
"description": "Don't declare MessageDigest as field (thread safety)",
"descriptionZh": "不要将 MessageDigest 声明为字段(线程安全)",
"descriptionJa": "MessageDigest をフィールドとして宣言しない(スレッド安全性)"
},
{
"id": "pmd/AvoidPrintStackTrace",
"description": "Use logger instead of printStackTrace()"
"description": "Use logger instead of printStackTrace()",
"descriptionZh": "使用 logger 替代 printStackTrace()",
"descriptionJa": "printStackTrace() の代わりにロガーを使用する"
},
{
"id": "pmd/AvoidReassigningCatchVariables",
"description": "Don't reassign caught exception variables"
"description": "Don't reassign caught exception variables",
"descriptionZh": "不要重新赋值捕获的异常变量",
"descriptionJa": "捕捉した例外変数に再代入しない"
},
{
"id": "pmd/AvoidReassigningLoopVariables",
"description": "Don't reassign loop control variables"
"description": "Don't reassign loop control variables",
"descriptionZh": "不要重新赋值循环控制变量",
"descriptionJa": "ループ制御変数に再代入しない"
},
{
"id": "pmd/AvoidReassigningParameters",
"description": "Don't reassign method parameters"
"description": "Don't reassign method parameters",
"descriptionZh": "不要重新赋值方法参数",
"descriptionJa": "メソッドパラメータに再代入しない"
},
{
"id": "pmd/AvoidStringBufferField",
"description": "Avoid StringBuffer/StringBuilder as fields"
"description": "Avoid StringBuffer/StringBuilder as fields",
"descriptionZh": "避免将 StringBuffer/StringBuilder 用作字段",
"descriptionJa": "StringBuffer/StringBuilder をフィールドとして使うことを避ける"
},
{
"id": "pmd/AvoidUsingHardCodedIP",
"description": "Externalize IP addresses"
"description": "Externalize IP addresses",
"descriptionZh": "外部化 IP 地址",
"descriptionJa": "IP アドレスを外部化する"
},
{
"id": "pmd/CheckResultSet",
"description": "Always check navigation method return values of ResultSet"
"description": "Always check navigation method return values of ResultSet",
"descriptionZh": "始终检查 ResultSet 导航方法的返回值",
"descriptionJa": "ResultSet のナビゲーションメソッドの戻り値を常に確認する"
},
{
"id": "pmd/ConstantsInInterface",
"description": "Avoid constants in interfaces"
"description": "Avoid constants in interfaces",
"descriptionZh": "避免在接口中定义常量",
"descriptionJa": "インターフェースでの定数定義を避ける"
},
{
"id": "pmd/DefaultLabelNotLastInSwitch",
"description": "Default label should be last in switch"
"description": "Default label should be last in switch",
"descriptionZh": "switch 中 default 标签应放在最后",
"descriptionJa": "switch では default ラベルを最後に置く"
},
{
"id": "pmd/DoubleBraceInitialization",
"description": "Avoid double-brace initialization"
"description": "Avoid double-brace initialization",
"descriptionZh": "避免双花括号初始化",
"descriptionJa": "二重波括弧初期化を避ける"
},
{
"id": "pmd/EnumComparison",
"description": "Compare enums with == not equals()"
"description": "Compare enums with == not equals()",
"descriptionZh": "使用 == 而非 equals() 比较枚举",
"descriptionJa": "列挙の比較には equals() ではなく == を使用する"
},
{
"id": "pmd/ExhaustiveSwitchHasDefault",
"description": "Exhaustive switch should not have default case"
"description": "Exhaustive switch should not have default case",
"descriptionZh": "穷尽式 switch 不应有 default 子句",
"descriptionJa": "網羅的な switch に default を置くべきでない"
},
{
"id": "pmd/ForLoopCanBeForeach",
"description": "Replace for loop with foreach"
"description": "Replace for loop with foreach",
"descriptionZh": "用 foreach 替代 for 循环",
"descriptionJa": "for ループを foreach に置き換える"
},
{
"id": "pmd/ForLoopVariableCount",
"description": "Limit control variables in for loops"
"description": "Limit control variables in for loops",
"descriptionZh": "限制 for 循环中的控制变量数量",
"descriptionJa": "for ループ内の制御変数の数を制限する"
},
{
"id": "pmd/GuardLogStatement",
"description": "Check log level before logging"
"description": "Check log level before logging",
"descriptionZh": "记录日志前检查日志级别",
"descriptionJa": "ログ出力前にログレベルを確認する"
},
{
"id": "pmd/ImplicitFunctionalInterface",
"description": "Annotate functional interfaces with @FunctionalInterface"
"description": "Annotate functional interfaces with @FunctionalInterface",
"descriptionZh": "用 @FunctionalInterface 注解函数式接口",
"descriptionJa": "関数型インターフェースに @FunctionalInterface を付ける"
},
{
"id": "pmd/JUnit4SuitesShouldUseSuiteAnnotation",
"description": "Use @RunWith(Suite.class) annotation"
"description": "Use @RunWith(Suite.class) annotation",
"descriptionZh": "使用 @RunWith(Suite.class) 注解",
"descriptionJa": "@RunWith(Suite.class) アノテーションを使用する"
},
{
"id": "pmd/JUnitJupiterTestShouldBePackagePrivate",
"description": "JUnit 5 tests should be package-private"
"description": "JUnit 5 tests should be package-private",
"descriptionZh": "JUnit 5 测试应为包私有",
"descriptionJa": "JUnit 5 のテストはパッケージプライベートにする"
},
{
"id": "pmd/JUnitUseExpected",
"description": "Use @Test(expected) annotation"
"description": "Use @Test(expected) annotation",
"descriptionZh": "使用 @Test(expected) 注解",
"descriptionJa": "@Test(expected) アノテーションを使用する"
},
{
"id": "pmd/LabeledStatement",
"description": "Avoid labeled statements"
"description": "Avoid labeled statements",
"descriptionZh": "避免带标签的语句",
"descriptionJa": "ラベル付き文を避ける"
},
{
"id": "pmd/LiteralsFirstInComparisons",
"description": "Position literals first in String comparisons"
"description": "Position literals first in String comparisons",
"descriptionZh": "字符串比较中将字面量放在前面",
"descriptionJa": "文字列比較ではリテラルを先頭に置く"
},
{
"id": "pmd/LooseCoupling",
"description": "Use interfaces instead of implementation types"
"description": "Use interfaces instead of implementation types",
"descriptionZh": "使用接口而非实现类型",
"descriptionJa": "実装型ではなくインターフェースを使用する"
},
{
"id": "pmd/MethodReturnsInternalArray",
"description": "Return copy of internal array"
"description": "Return copy of internal array",
"descriptionZh": "返回内部数组的副本",
"descriptionJa": "内部配列のコピーを返す"
},
{
"id": "pmd/MissingOverride",
"description": "Add @Override annotation"
"description": "Add @Override annotation",
"descriptionZh": "添加 @Override 注解",
"descriptionJa": "@Override アノテーションを追加する"
},
{
"id": "pmd/NonExhaustiveSwitch",
"description": "Switch should be exhaustive"
"description": "Switch should be exhaustive",
"descriptionZh": "switch 应为穷尽式",
"descriptionJa": "switch を網羅的にする"
},
{
"id": "pmd/OneDeclarationPerLine",
"description": "One declaration per line"
"description": "One declaration per line",
"descriptionZh": "每行一个声明",
"descriptionJa": "1行に1つの宣言"
},
{
"id": "pmd/PreserveStackTrace",
"description": "Preserve stack trace when rethrowing exceptions"
"description": "Preserve stack trace when rethrowing exceptions",
"descriptionZh": "重新抛出异常时保留堆栈跟踪",
"descriptionJa": "例外を再スローする際にスタックトレースを保持する"
},
{
"id": "pmd/PrimitiveWrapperInstantiation",
"description": "Use valueOf() instead of new Type()"
"description": "Use valueOf() instead of new Type()",
"descriptionZh": "使用 valueOf() 而非 new Type()",
"descriptionJa": "new Type() の代わりに valueOf() を使用する"
},
{
"id": "pmd/RelianceOnDefaultCharset",
"description": "Specify charset explicitly"
"description": "Specify charset explicitly",
"descriptionZh": "显式指定字符集",
"descriptionJa": "文字セットを明示的に指定する"
},
{
"id": "pmd/ReplaceEnumerationWithIterator",
"description": "Use Iterator instead of Enumeration"
"description": "Use Iterator instead of Enumeration",
"descriptionZh": "使用 Iterator 替代 Enumeration",
"descriptionJa": "Enumeration の代わりに Iterator を使用する"
},
{
"id": "pmd/ReplaceHashtableWithMap",
"description": "Use Map instead of Hashtable"
"description": "Use Map instead of Hashtable",
"descriptionZh": "使用 Map 替代 Hashtable",
"descriptionJa": "Hashtable の代わりに Map を使用する"
},
{
"id": "pmd/ReplaceVectorWithList",
"description": "Use List/ArrayList instead of Vector"
"description": "Use List/ArrayList instead of Vector",
"descriptionZh": "使用 List/ArrayList 替代 Vector",
"descriptionJa": "Vector の代わりに List/ArrayList を使用する"
},
{
"id": "pmd/ReturnEmptyCollectionRatherThanNull",
"description": "Return empty collection rather than null"
"description": "Return empty collection rather than null",
"descriptionZh": "返回空集合而非 null",
"descriptionJa": "null ではなく空のコレクションを返す"
},
{
"id": "pmd/SimplifiableTestAssertion",
"description": "Use more specific assertion methods"
"description": "Use more specific assertion methods",
"descriptionZh": "使用更具体的断言方法",
"descriptionJa": "より具体的なアサーションメソッドを使用する"
},
{
"id": "pmd/SystemPrintln",
"description": "Use logger instead of System.out/err"
"description": "Use logger instead of System.out/err",
"descriptionZh": "使用 logger 替代 System.out/err",
"descriptionJa": "System.out/err の代わりにロガーを使用する"
},
{
"id": "pmd/UnitTestAssertionsShouldIncludeMessage",
"description": "Include message in assertions"
"description": "Include message in assertions",
"descriptionZh": "断言中包含消息",
"descriptionJa": "アサーションにメッセージを含める"
},
{
"id": "pmd/UnitTestContainsTooManyAsserts",
"description": "Limit asserts per test"
"description": "Limit asserts per test",
"descriptionZh": "限制每个测试的断言数量",
"descriptionJa": "テストごとのアサーション数を制限する"
},
{
"id": "pmd/UnitTestShouldIncludeAssert",
"description": "Test should include assertions"
"description": "Test should include assertions",
"descriptionZh": "测试应包含断言",
"descriptionJa": "テストにアサーションを含めるべきである"
},
{
"id": "pmd/UnitTestShouldUseAfterAnnotation",
"description": "Use @After/@AfterEach annotation"
"description": "Use @After/@AfterEach annotation",
"descriptionZh": "使用 @After/@AfterEach 注解",
"descriptionJa": "@After/@AfterEach アノテーションを使用する"
},
{
"id": "pmd/UnitTestShouldUseBeforeAnnotation",
"description": "Use @Before/@BeforeEach annotation"
"description": "Use @Before/@BeforeEach annotation",
"descriptionZh": "使用 @Before/@BeforeEach 注解",
"descriptionJa": "@Before/@BeforeEach アノテーションを使用する"
},
{
"id": "pmd/UnitTestShouldUseTestAnnotation",
"description": "Use @Test annotation"
"description": "Use @Test annotation",
"descriptionZh": "使用 @Test 注解",
"descriptionJa": "@Test アノテーションを使用する"
},
{
"id": "pmd/UnnecessaryVarargsArrayCreation",
"description": "Don't create explicit array for varargs"
"description": "Don't create explicit array for varargs",
"descriptionZh": "不要为可变参数创建显式数组",
"descriptionJa": "可変長引数用に明示的な配列を作成しない"
},
{
"id": "pmd/UnnecessaryWarningSuppression",
"description": "Remove unused PMD suppressions"
"description": "Remove unused PMD suppressions",
"descriptionZh": "移除未使用的 PMD 抑制",
"descriptionJa": "未使用の PMD 抑制を削除する"
},
{
"id": "pmd/UnsynchronizedStaticFormatter",
"description": "Static formatter should be synchronized"
"description": "Static formatter should be synchronized",
"descriptionZh": "静态 formatter 应同步",
"descriptionJa": "静的フォーマッタは同期化すべきである"
},
{
"id": "pmd/UnusedAssignment",
"description": "Remove unused assignments"
"description": "Remove unused assignments",
"descriptionZh": "移除未使用的赋值",
"descriptionJa": "未使用の代入を削除する"
},
{
"id": "pmd/UnusedFormalParameter",
"description": "Remove unused parameters"
"description": "Remove unused parameters",
"descriptionZh": "移除未使用的参数",
"descriptionJa": "未使用のパラメータを削除する"
},
{
"id": "pmd/UnusedLabel",
"description": "Remove unused labels"
"description": "Remove unused labels",
"descriptionZh": "移除未使用的标签",
"descriptionJa": "未使用のラベルを削除する"
},
{
"id": "pmd/UnusedLocalVariable",
"description": "Remove unused local variables"
"description": "Remove unused local variables",
"descriptionZh": "移除未使用的局部变量",
"descriptionJa": "未使用のローカル変数を削除する"
},
{
"id": "pmd/UnusedPrivateField",
"description": "Remove unused private fields"
"description": "Remove unused private fields",
"descriptionZh": "移除未使用的私有字段",
"descriptionJa": "未使用のプライベートフィールドを削除する"
},
{
"id": "pmd/UnusedPrivateMethod",
"description": "Remove unused private methods"
"description": "Remove unused private methods",
"descriptionZh": "移除未使用的私有方法",
"descriptionJa": "未使用のプライベートメソッドを削除する"
},
{
"id": "pmd/UseCollectionIsEmpty",
"description": "Use isEmpty() instead of size()==0"
"description": "Use isEmpty() instead of size()==0",
"descriptionZh": "使用 isEmpty() 替代 size()==0",
"descriptionJa": "size()==0 の代わりに isEmpty() を使用する"
},
{
"id": "pmd/UseEnumCollections",
"description": "Use EnumSet/EnumMap instead of HashSet/HashMap"
"description": "Use EnumSet/EnumMap instead of HashSet/HashMap",
"descriptionZh": "使用 EnumSet/EnumMap 替代 HashSet/HashMap",
"descriptionJa": "HashSet/HashMap の代わりに EnumSet/EnumMap を使用する"
},
{
"id": "pmd/UseStandardCharsets",
"description": "Use StandardCharsets constants"
"description": "Use StandardCharsets constants",
"descriptionZh": "使用 StandardCharsets 常量",
"descriptionJa": "StandardCharsets 定数を使用する"
},
{
"id": "pmd/UseTryWithResources",
"description": "Use try-with-resources"
"description": "Use try-with-resources",
"descriptionZh": "使用 try-with-resources",
"descriptionJa": "try-with-resources を使用する"
},
{
"id": "pmd/UseUtilityClass",
"description": "Utility class should have private constructor"
"description": "Utility class should have private constructor",
"descriptionZh": "工具类应有私有构造函数",
"descriptionJa": "ユーティリティクラスはプライベートコンストラクタを持つべきである"
},
{
"id": "pmd/UseVarargs",
"description": "Use varargs instead of array parameter"
"description": "Use varargs instead of array parameter",
"descriptionZh": "使用可变参数替代数组参数",
"descriptionJa": "配列パラメータの代わりに可変長引数を使用する"
},
{
"id": "pmd/VariableCanBeInlined",
"description": "Variable can be inlined"
"description": "Variable can be inlined",
"descriptionZh": "变量可以内联",
"descriptionJa": "変数をインライン化できる"
},
{
"id": "pmd/WhileLoopWithLiteralBoolean",
"description": "Simplify while loops with literal booleans"
"description": "Simplify while loops with literal booleans",
"descriptionZh": "简化带字面量布尔值的 while 循环",
"descriptionJa": "リテラルブール値を持つ while ループを簡略化する"
},
{
"id": "pmd/AtLeastOneConstructor",
"description": "Each class should have a constructor"
"description": "Each class should have a constructor",
"descriptionZh": "每个类都应有一个构造函数",
"descriptionJa": "各クラスにコンストラクタを1つ持つべきである"
},
{
"id": "pmd/AvoidDollarSigns",
"description": "Avoid $ in names"
"description": "Avoid $ in names",
"descriptionZh": "避免在名称中使用 $",
"descriptionJa": "名前に $ を使用することを避ける"
},
{
"id": "pmd/AvoidProtectedFieldInFinalClass",
"description": "Don't use protected fields in final classes"
"description": "Don't use protected fields in final classes",
"descriptionZh": "final 类中不要使用 protected 字段",
"descriptionJa": "final クラスで protected フィールドを使わない"
},
{
"id": "pmd/AvoidProtectedMethodInFinalClassNotExtending",
"description": "Don't use protected methods in final classes not extending"
"description": "Don't use protected methods in final classes not extending",
"descriptionZh": "非继承的 final 类中不要使用 protected 方法",
"descriptionJa": "継承しない final クラスで protected メソッドを使わない"
},
{
"id": "pmd/AvoidUsingNativeCode",
"description": "Avoid JNI calls"
"description": "Avoid JNI calls",
"descriptionZh": "避免 JNI 调用",
"descriptionJa": "JNI 呼び出しを避ける"
},
{
"id": "pmd/BooleanGetMethodName",
"description": "Boolean getters should be named isX()"
"description": "Boolean getters should be named isX()",
"descriptionZh": "布尔 getter 应命名为 isX()",
"descriptionJa": "ブールゲッターは isX() と命名する"
},
{
"id": "pmd/CallSuperInConstructor",
"description": "Call super() in constructor"
"description": "Call super() in constructor",
"descriptionZh": "在构造函数中调用 super()",
"descriptionJa": "コンストラクタで super() を呼び出す"
},
{
"id": "pmd/ClassNamingConventions",
"description": "PascalCase naming"
"description": "PascalCase naming",
"descriptionZh": "PascalCase 命名",
"descriptionJa": "PascalCase 命名"
},
{
"id": "pmd/CommentDefaultAccessModifier",
"description": "Comment default access modifier"
"description": "Comment default access modifier",
"descriptionZh": "注释默认访问修饰符",
"descriptionJa": "デフォルトアクセス修飾子をコメントする"
},
{
"id": "pmd/ConfusingTernary",
"description": "Avoid negation in if with else"
"description": "Avoid negation in if with else",
"descriptionZh": "避免在带 else 的 if 中使用取反",
"descriptionJa": "else 付き if での否定を避ける"
},
{
"id": "pmd/ControlStatementBraces",
"description": "Require braces on control statements"
"description": "Require braces on control statements",
"descriptionZh": "控制语句要求花括号",
"descriptionJa": "制御文に波括弧を要求する"
},
{
"id": "pmd/EmptyControlStatement",
"description": "Report empty control statements"
"description": "Report empty control statements",
"descriptionZh": "报告空的控制语句",
"descriptionJa": "空の制御文を報告する"
},
{
"id": "pmd/EmptyMethodInAbstractClassShouldBeAbstract",
"description": "Empty methods in abstract classes should be abstract"
"description": "Empty methods in abstract classes should be abstract",
"descriptionZh": "抽象类中的空方法应为抽象方法",
"descriptionJa": "抽象クラスの空メソッドは抽象にすべきである"
},
{
"id": "pmd/ExtendsObject",
"description": "No need to explicitly extend Object"
"description": "No need to explicitly extend Object",
"descriptionZh": "无需显式继承 Object",
"descriptionJa": "Object を明示的に継承する必要はない"
},
{
"id": "pmd/FieldDeclarationsShouldBeAtStartOfClass",
"description": "Fields at top of class"
"description": "Fields at top of class",
"descriptionZh": "字段放在类的顶部",
"descriptionJa": "フィールドをクラスの先頭に置く"
},
{
"id": "pmd/FieldNamingConventions",
"description": "Configurable field naming conventions"
"description": "Configurable field naming conventions",
"descriptionZh": "可配置的字段命名规范",
"descriptionJa": "設定可能なフィールド命名規則"
},
{
"id": "pmd/FinalParameterInAbstractMethod",
"description": "Final parameter in abstract method is useless"
"description": "Final parameter in abstract method is useless",
"descriptionZh": "抽象方法中的 final 参数无用",
"descriptionJa": "抽象メソッドの final パラメータは無意味である"
},
{
"id": "pmd/ForLoopShouldBeWhileLoop",
"description": "Simplify for loops to while"
"description": "Simplify for loops to while",
"descriptionZh": "将 for 循环简化为 while",
"descriptionJa": "for ループを while に簡略化する"
},
{
"id": "pmd/FormalParameterNamingConventions",
"description": "Parameter naming conventions"
"description": "Parameter naming conventions",
"descriptionZh": "参数命名规范",
"descriptionJa": "パラメータ命名規則"
},
{
"id": "pmd/IdenticalCatchBranches",
"description": "Collapse identical catch branches"
"description": "Collapse identical catch branches",
"descriptionZh": "合并相同的 catch 分支",
"descriptionJa": "同一の catch ブランチを統合する"
},
{
"id": "pmd/LambdaCanBeMethodReference",
"description": "Replace lambda with method reference"
"description": "Replace lambda with method reference",
"descriptionZh": "用方法引用替代 lambda",
"descriptionJa": "ラムダをメソッド参照に置き換える"
},
{
"id": "pmd/LinguisticNaming",
"description": "Method name/return type consistency"
"description": "Method name/return type consistency",
"descriptionZh": "方法名与返回类型一致性",
"descriptionJa": "メソッド名と戻り値型の整合性"
},
{
"id": "pmd/LocalHomeNamingConvention",
"description": "EJB LocalHome suffix"
"description": "EJB LocalHome suffix",
"descriptionZh": "EJB LocalHome 后缀",
"descriptionJa": "EJB LocalHome サフィックス"
},
{
"id": "pmd/LocalInterfaceSessionNamingConvention",
"description": "EJB Local suffix"
"description": "EJB Local suffix",
"descriptionZh": "EJB Local 后缀",
"descriptionJa": "EJB Local サフィックス"
},
{
"id": "pmd/LocalVariableCouldBeFinal",
"description": "Declare local variables final when possible"
"description": "Declare local variables final when possible",
"descriptionZh": "尽可能将局部变量声明为 final",
"descriptionJa": "可能な限りローカル変数を final で宣言する"
},
{
"id": "pmd/LocalVariableNamingConventions",
"description": "Variable naming conventions"
"description": "Variable naming conventions",
"descriptionZh": "变量命名规范",
"descriptionJa": "変数命名規則"
},
{
"id": "pmd/LongVariable",
"description": "Avoid excessively long variable names (>17 chars)"
"description": "Avoid excessively long variable names (>17 chars)",
"descriptionZh": "避免过长的变量名(超过17个字符)",
"descriptionJa": "過度に長い変数名(17文字超)を避ける"
},
{
"id": "pmd/MDBAndSessionBeanNamingConvention",
"description": "EJB Bean suffix"
"description": "EJB Bean suffix",
"descriptionZh": "EJB Bean 后缀",
"descriptionJa": "EJB Bean サフィックス"
},
{
"id": "pmd/MethodArgumentCouldBeFinal",
"description": "Declare parameters final when possible"
"description": "Declare parameters final when possible",
"descriptionZh": "尽可能将参数声明为 final",
"descriptionJa": "可能な限りパラメータを final で宣言する"
},
{
"id": "pmd/MethodNamingConventions",
"description": "Method naming conventions"
"description": "Method naming conventions",
"descriptionZh": "方法命名规范",
"descriptionJa": "メソッド命名規則"
},
{
"id": "pmd/ModifierOrder",
"description": "Enforce JLS modifier order"
"description": "Enforce JLS modifier order",
"descriptionZh": "强制 JLS 修饰符顺序",
"descriptionJa": "JLS 修飾子の順序を強制する"
},
{
"id": "pmd/NoPackage",
"description": "All types must belong to a named package"
"description": "All types must belong to a named package",
"descriptionZh": "所有类型必须属于命名包",
"descriptionJa": "すべての型は名前付きパッケージに属すべきである"
},
{
"id": "pmd/OnlyOneReturn",
"description": "Single exit point per method"
"description": "Single exit point per method",
"descriptionZh": "每个方法只有一个出口",
"descriptionJa": "メソッドに出口を1つだけ持たせる"
},
{
"id": "pmd/PackageCase",
"description": "Package names lowercase"
"description": "Package names lowercase",
"descriptionZh": "包名使用小写",
"descriptionJa": "パッケージ名は小文字にする"
},
{
"id": "pmd/PrematureDeclaration",
"description": "Declare variables close to usage"
"description": "Declare variables close to usage",
"descriptionZh": "变量声明靠近使用处",
"descriptionJa": "変数を使用箇所の近くで宣言する"
},
{
"id": "pmd/UselessParentheses",
"description": "Remove unnecessary parentheses"
"description": "Remove unnecessary parentheses",
"descriptionZh": "移除不必要的括号",
"descriptionJa": "不要な括弧を削除する"
},
{
"id": "pmd/UselessQualifiedThis",
"description": "Remove unnecessary qualified this"
"description": "Remove unnecessary qualified this",
"descriptionZh": "移除不必要的限定 this",
"descriptionJa": "不要な限定 this を削除する"
},
{
"id": "pmd/UnnecessaryAnnotationValueElement",
"description": "Remove unnecessary annotation value element"
"description": "Remove unnecessary annotation value element",
"descriptionZh": "移除不必要的注解值元素",
"descriptionJa": "不要なアノテーション値要素を削除する"
},
{
"id": "pmd/UnnecessaryBoxing",
"description": "Avoid unnecessary boxing"
"description": "Avoid unnecessary boxing",
"descriptionZh": "避免不必要的装箱",
"descriptionJa": "不要なボクシングを避ける"
},
{
"id": "pmd/UnnecessaryCast",
"description": "Remove unnecessary casts"
"description": "Remove unnecessary casts",
"descriptionZh": "移除不必要的强制转换",
"descriptionJa": "不要なキャストを削除する"
},
{
"id": "pmd/UnnecessaryConstructor",
"description": "Remove unnecessary constructors"
"description": "Remove unnecessary constructors",
"descriptionZh": "移除不必要的构造函数",
"descriptionJa": "不要なコンストラクタを削除する"
},
{
"id": "pmd/UnnecessaryFullyQualifiedName",
"description": "Remove unnecessary fully qualified names"
"description": "Remove unnecessary fully qualified names",
"descriptionZh": "移除不必要的全限定名",
"descriptionJa": "不要な完全修飾名を削除する"
},
{
"id": "pmd/UnnecessaryImport",
"description": "Remove unnecessary imports"
"description": "Remove unnecessary imports",
"descriptionZh": "移除不必要的导入",
"descriptionJa": "不要なインポートを削除する"
},
{
"id": "pmd/UnnecessaryModifier",
"description": "Remove unnecessary modifiers"
"description": "Remove unnecessary modifiers",
"descriptionZh": "移除不必要的修饰符",
"descriptionJa": "不要な修飾子を削除する"
},
{
"id": "pmd/UnnecessaryReturn",
"description": "Remove unnecessary returns"
"description": "Remove unnecessary returns",
"descriptionZh": "移除不必要的 return",
"descriptionJa": "不要な return を削除する"
},
{
"id": "pmd/UnnecessarySemicolon",
"description": "Remove unnecessary semicolons"
"description": "Remove unnecessary semicolons",
"descriptionZh": "移除不必要的分号",
"descriptionJa": "不要なセミコロンを削除する"
},
{
"id": "pmd/UnnecessaryUnboxing",
"description": "Avoid unnecessary unboxing"
"description": "Avoid unnecessary unboxing",
"descriptionZh": "避免不必要的拆箱",
"descriptionJa": "不要なアンボクシングを避ける"
},
{
"id": "pmd/UpperLowerCaseNamingConventions",
"description": "Naming conventions for cases"
"description": "Naming conventions for cases",
"descriptionZh": "大小写命名规范",
"descriptionJa": "大文字・小文字の命名規則"
},
{
"id": "pmd/UseShortArrayInitializer",
"description": "Use short array initializer"
"description": "Use short array initializer",
"descriptionZh": "使用简短的数组初始化器",
"descriptionJa": "簡潔な配列初期化子を使用する"
},
{
"id": "pmd/AbstractClassWithoutAnyMethod",
"description": "Abstract class without methods should use private constructor"
"description": "Abstract class without methods should use private constructor",
"descriptionZh": "没有任何方法的抽象类应使用私有构造函数",
"descriptionJa": "メソッドのない抽象クラスはプライベートコンストラクタを使うべきである"
},
{
"id": "pmd/AvoidDeeplyNestedIfStmts",
"description": "Avoid deeply nested if statements"
"description": "Avoid deeply nested if statements",
"descriptionZh": "避免深度嵌套的 if 语句",
"descriptionJa": "深くネストした if 文を避ける"
},
{
"id": "pmd/AvoidRethrowingException",
"description": "Avoid catch-and-rethrow"
"description": "Avoid catch-and-rethrow",
"descriptionZh": "避免捕获后重新抛出",
"descriptionJa": "catch-and-rethrow を避ける"
},
{
"id": "pmd/AvoidThrowingNewInstanceOfSameException",
"description": "Avoid wrapping same exception type"
"description": "Avoid wrapping same exception type",
"descriptionZh": "避免包装相同异常类型",
"descriptionJa": "同じ例外型のラップを避ける"
},
{
"id": "pmd/AvoidThrowingNullPointerException",
"description": "Don't throw NPE manually"
"description": "Don't throw NPE manually",
"descriptionZh": "不要手动抛出 NPE",
"descriptionJa": "NPE を手動で投げない"
},
{
"id": "pmd/AvoidThrowingRawExceptionTypes",
"description": "Don't throw raw Exception/RuntimeException/Throwable/Error"
"description": "Don't throw raw Exception/RuntimeException/Throwable/Error",
"descriptionZh": "不要抛出原始 Exception/RuntimeException/Throwable/Error",
"descriptionJa": "生の Exception/RuntimeException/Throwable/Error を投げない"
},
{
"id": "pmd/AvoidUncheckedExceptionsInSignatures",
"description": "Don't declare unchecked exceptions in throws"
"description": "Don't declare unchecked exceptions in throws",
"descriptionZh": "不要在 throws 中声明非受检异常",
"descriptionJa": "throws で非チェック例外を宣言しない"
},
{
"id": "pmd/ClassWithOnlyPrivateConstructorsShouldBeFinal",
"description": "Make class final if only private constructors"
"description": "Make class final if only private constructors",
"descriptionZh": "只有私有构造函数的类应为 final",
"descriptionJa": "プライベートコンストラクタのみのクラスは final にする"
},
{
"id": "pmd/CognitiveComplexity",
"description": "Methods with high cognitive complexity"
"description": "Methods with high cognitive complexity",
"descriptionZh": "高认知复杂度的方法",
"descriptionJa": "認知複雑度の高いメソッド"
},
{
"id": "pmd/CollapsibleIfStatements",
"description": "Merge nested if statements"
"description": "Merge nested if statements",
"descriptionZh": "合并嵌套的 if 语句",
"descriptionJa": "ネストした if 文を統合する"
},
{
"id": "pmd/CouplingBetweenObjects",
"description": "High coupling threshold"
"description": "High coupling threshold",
"descriptionZh": "高耦合阈值",
"descriptionJa": "高い結合度の閾値"
},
{
"id": "pmd/CyclomaticComplexity",
"description": "High cyclomatic complexity"
"description": "High cyclomatic complexity",
"descriptionZh": "高圈复杂度",
"descriptionJa": "高い循環的複雑度"
},
{
"id": "pmd/DataClass",
"description": "Suspected Data Class"
"description": "Suspected Data Class",
"descriptionZh": "疑似数据类",
"descriptionJa": "疑わしいデータクラス"
},
{
"id": "pmd/DoNotExtendJavaLangError",
"description": "Don't extend Error"
"description": "Don't extend Error",
"descriptionZh": "不要继承 Error",
"descriptionJa": "Error を継承しない"
},
{
"id": "pmd/ExceptionAsFlowControl",
"description": "Don't use exceptions for flow control"
"description": "Don't use exceptions for flow control",
"descriptionZh": "不要使用异常控制流程",
"descriptionJa": "制御フローに例外を使わない"
},
{
"id": "pmd/ExcessiveImports",
"description": "Too many imports"
"description": "Too many imports",
"descriptionZh": "导入过多",
"descriptionJa": "インポートが多すぎる"
},
{
"id": "pmd/ExcessiveParameterList",
"description": "Too many parameters"
"description": "Too many parameters",
"descriptionZh": "参数过多",
"descriptionJa": "パラメータが多すぎる"
},
{
"id": "pmd/ExcessivePublicCount",
"description": "Too many public methods/attributes"
"description": "Too many public methods/attributes",
"descriptionZh": "公共方法/属性过多",
"descriptionJa": "public メソッド/属性が多すぎる"
},
{
"id": "pmd/FinalFieldCouldBeStatic",
"description": "Make final field static if compile-time constant"
"description": "Make final field static if compile-time constant",
"descriptionZh": "编译时常量 final 字段应为 static",
"descriptionJa": "コンパイル時定数の final フィールドは static にできる"
},
{
"id": "pmd/GodClass",
"description": "God Class detection"
"description": "God Class detection",
"descriptionZh": "上帝类检测",
"descriptionJa": "God クラスの検出"
},
{
"id": "pmd/ImmutableField",
"description": "Field could be final"
"description": "Field could be final",
"descriptionZh": "字段可以声明为 final",
"descriptionJa": "フィールドは final にできる"
},
{
"id": "pmd/InvalidJavaBean",
"description": "Bean doesn't follow JavaBeans spec"
"description": "Bean doesn't follow JavaBeans spec",
"descriptionZh": "Bean 不符合 JavaBeans 规范",
"descriptionJa": "Bean が JavaBeans 仕様に従っていない"
},
{
"id": "pmd/LawOfDemeter",
"description": "Potential LoD violation"
"description": "Potential LoD violation",
"descriptionZh": "潜在的迪米特法则违规",
"descriptionJa": "潜在的な LoD 違反"
},
{
"id": "pmd/LogicInversion",
"description": "Use opposite operator instead of !"
"description": "Use opposite operator instead of !",
"descriptionZh": "使用相反的运算符替代 !",
"descriptionJa": "! の代わりに反対の演算子を使用する"
},
{
"id": "pmd/LoosePackageCoupling",
"description": "Avoid using classes from outside package hierarchy"
"description": "Avoid using classes from outside package hierarchy",
"descriptionZh": "避免使用包层次之外的类",
"descriptionJa": "パッケージ階層外のクラスの使用を避ける"
},
{
"id": "pmd/MutableStaticState",
"description": "Non-private non-final static fields"
"description": "Non-private non-final static fields",
"descriptionZh": "非私有非 final 的静态字段",
"descriptionJa": "非 private かつ非 final の静的フィールド"
},
{
"id": "pmd/NcssCount",
"description": "Non-Commenting Source Statements metric"
"description": "Non-Commenting Source Statements metric",
"descriptionZh": "非注释源码语句度量",
"descriptionJa": "非コメントソース文のメトリクス"
},
{
"id": "pmd/NPathComplexity",
"description": "NPath complexity threshold"
"description": "NPath complexity threshold",
"descriptionZh": "NPath 复杂度阈值",
"descriptionJa": "NPath 複雑度の閾値"
},
{
"id": "pmd/PublicMemberInNonPublicType",
"description": "Public member in non-public type"
"description": "Public member in non-public type",
"descriptionZh": "非公共类型中的公共成员",
"descriptionJa": "非 public 型内の public メンバー"
},
{
"id": "pmd/SignatureDeclareThrowsException",
"description": "Don't declare throws Exception"
"description": "Don't declare throws Exception",
"descriptionZh": "不要声明 throws Exception",
"descriptionJa": "throws Exception を宣言しない"
},
{
"id": "pmd/SimplifiedTernary",
"description": "Simplify ternary with boolean literals"
"description": "Simplify ternary with boolean literals",
"descriptionZh": "用布尔字面量简化三元表达式",
"descriptionJa": "ブールリテラルで三項演算子を簡略化する"
},
{
"id": "pmd/SimplifyBooleanExpressions",
"description": "Remove unnecessary boolean comparisons"
"description": "Remove unnecessary boolean comparisons",
"descriptionZh": "移除不必要的布尔比较",
"descriptionJa": "不要なブール比較を削除する"
},
{
"id": "pmd/SimplifyBooleanReturns",
"description": "Simplify boolean returns"
"description": "Simplify boolean returns",
"descriptionZh": "简化布尔返回",
"descriptionJa": "ブールの戻り値を簡略化する"
},
{
"id": "pmd/SimplifyConditional",
"description": "Simplify conditional expressions"
"description": "Simplify conditional expressions",
"descriptionZh": "简化条件表达式",
"descriptionJa": "条件式を簡略化する"
},
{
"id": "pmd/SingularField",
"description": "Field may be local variable"
"description": "Field may be local variable",
"descriptionZh": "字段可能应为局部变量",
"descriptionJa": "フィールドはローカル変数にできる"
},
{
"id": "pmd/TooManyFields",
"description": "Too many fields"
"description": "Too many fields",
"descriptionZh": "字段过多",
"descriptionJa": "フィールドが多すぎる"
},
{
"id": "pmd/TooManyMethods",
"description": "Too many methods"
"description": "Too many methods",
"descriptionZh": "方法过多",
"descriptionJa": "メソッドが多すぎる"
},
{
"id": "pmd/UselessOverridingMethod",
"description": "Useless overriding method"
"description": "Useless overriding method",
"descriptionZh": "无意义的重写方法",
"descriptionJa": "無意味なオーバーライドメソッド"
},
{
"id": "pmd/AssertEqualsArgumentOrder",
"description": "assertEquals expected/actual swapped"
"description": "assertEquals expected/actual swapped",
"descriptionZh": "assertEquals 的 expected/actual 参数顺序颠倒",
"descriptionJa": "assertEquals の expected/actual 引数が逆"
},
{
"id": "pmd/AssignmentInOperand",
"description": "Avoid assignments in operands"
"description": "Avoid assignments in operands",
"descriptionZh": "避免在操作数中赋值",
"descriptionJa": "オペランド内での代入を避ける"
},
{
"id": "pmd/AssignmentToNonFinalStatic",
"description": "Unsafe static field assignment in constructor"
"description": "Unsafe static field assignment in constructor",
"descriptionZh": "构造函数中对非 final 静态字段的不安全赋值",
"descriptionJa": "コンストラクタ内の非 final 静的フィールドへの安全でない代入"
},
{
"id": "pmd/AvoidAccessibilityAlteration",
"description": "Don't use setAccessible(true)"
"description": "Don't use setAccessible(true)",
"descriptionZh": "不要使用 setAccessible(true)",
"descriptionJa": "setAccessible(true) を使用しない"
},
{
"id": "pmd/AvoidAssertAsIdentifier",
"description": "assert is reserved word (Java <1.4)"
"description": "assert is reserved word (Java <1.4)",
"descriptionZh": "assert 是保留字(Java <1.4",
"descriptionJa": "assert は予約語である(Java <1.4"
},
{
"id": "pmd/AvoidBranchingStatementAsLastInLoop",
"description": "Branching statement as last in loop"
"description": "Branching statement as last in loop",
"descriptionZh": "循环体最后的跳转语句",
"descriptionJa": "ループの最後の分岐文"
},
{
"id": "pmd/AvoidCallingFinalize",
"description": "Don't call finalize() explicitly"
"description": "Don't call finalize() explicitly",
"descriptionZh": "不要显式调用 finalize()",
"descriptionJa": "finalize() を明示的に呼ばない"
},
{
"id": "pmd/AvoidCatchingGenericException",
"description": "Don't catch generic exceptions"
"description": "Don't catch generic exceptions",
"descriptionZh": "不要捕获泛化异常",
"descriptionJa": "汎用例外を捕捉しない"
},
{
"id": "pmd/AvoidDecimalLiteralsInBigDecimalConstructor",
"description": "Use String constructor for BigDecimal"
"description": "Use String constructor for BigDecimal",
"descriptionZh": "BigDecimal 使用 String 构造函数",
"descriptionJa": "BigDecimal には String コンストラクタを使用する"
},
{
"id": "pmd/AvoidDuplicateLiterals",
"description": "Avoid duplicate String literals"
"description": "Avoid duplicate String literals",
"descriptionZh": "避免重复的 String 字面量",
"descriptionJa": "重複する文字列リテラルを避ける"
},
{
"id": "pmd/AvoidEnumAsIdentifier",
"description": "enum is reserved word (Java <1.5)"
"description": "enum is reserved word (Java <1.5)",
"descriptionZh": "enum 是保留字(Java <1.5",
"descriptionJa": "enum は予約語である(Java <1.5"
},
{
"id": "pmd/AvoidFieldNameMatchingMethodName",
"description": "Field name matching method name"
"description": "Field name matching method name",
"descriptionZh": "字段名与方法名相同",
"descriptionJa": "フィールド名とメソッド名が一致する"
},
{
"id": "pmd/AvoidFieldNameMatchingTypeName",
"description": "Field name matching type name"
"description": "Field name matching type name",
"descriptionZh": "字段名与类型名相同",
"descriptionJa": "フィールド名と型名が一致する"
},
{
"id": "pmd/AvoidInstanceofChecksInCatchClause",
"description": "Use separate catch clauses"
"description": "Use separate catch clauses",
"descriptionZh": "使用单独的 catch 子句",
"descriptionJa": "個別の catch 句を使用する"
},
{
"id": "pmd/AvoidLiteralsInIfCondition",
"description": "Avoid magic numbers in if conditions"
"description": "Avoid magic numbers in if conditions",
"descriptionZh": "避免 if 条件中的魔术数字",
"descriptionJa": "if 条件内のマジックナンバーを避ける"
},
{
"id": "pmd/AvoidMultipleUnaryOperators",
"description": "Avoid multiple unary operators"
"description": "Avoid multiple unary operators",
"descriptionZh": "避免多个一元运算符",
"descriptionJa": "複数の単項演算子を避ける"
},
{
"id": "pmd/AvoidSynchronizedStatement",
"description": "Avoid synchronized statements"
"description": "Avoid synchronized statements",
"descriptionZh": "避免 synchronized 语句",
"descriptionJa": "synchronized 文を避ける"
},
{
"id": "pmd/AvoidSynchronizedAtMethodLevel",
"description": "Avoid synchronized at method level"
"description": "Avoid synchronized at method level",
"descriptionZh": "避免在方法级别使用 synchronized",
"descriptionJa": "メソッドレベルでの synchronized を避ける"
},
{
"id": "pmd/AvoidThreadGroup",
"description": "Avoid using ThreadGroup"
"description": "Avoid using ThreadGroup",
"descriptionZh": "避免使用 ThreadGroup",
"descriptionJa": "ThreadGroup の使用を避ける"
},
{
"id": "pmd/AvoidUsingOctalValues",
"description": "Avoid octal literals"
"description": "Avoid octal literals",
"descriptionZh": "避免八进制字面量",
"descriptionJa": "8進数リテラルを避ける"
},
{
"id": "pmd/AvoidUsingVolatile",
"description": "Avoid the volatile keyword"
"description": "Avoid the volatile keyword",
"descriptionZh": "避免 volatile 关键字",
"descriptionJa": "volatile キーワードを避ける"
},
{
"id": "pmd/BrokenNullCheck",
"description": "Broken null check (|| vs &&)"
"description": "Broken null check (|| vs &&)",
"descriptionZh": "错误的 null 检查(|| 与 &&",
"descriptionJa": "壊れた null チェック(|| vs &&"
},
{
"id": "pmd/CallSuperFirst",
"description": "super should be called first"
"description": "super should be called first",
"descriptionZh": "super 应首先调用",
"descriptionJa": "super を最初に呼ぶべきである"
},
{
"id": "pmd/CallSuperLast",
"description": "super should be called last"
"description": "super should be called last",
"descriptionZh": "super 应最后调用",
"descriptionJa": "super を最後に呼ぶべきである"
},
{
"id": "pmd/CheckSkipResult",
"description": "Check skip() return value"
"description": "Check skip() return value",
"descriptionZh": "检查 skip() 的返回值",
"descriptionJa": "skip() の戻り値を確認する"
},
{
"id": "pmd/ClassCastExceptionWithToArray",
"description": "Collection.toArray() ClassCastException"
"description": "Collection.toArray() ClassCastException",
"descriptionZh": "Collection.toArray() 的 ClassCastException",
"descriptionJa": "Collection.toArray() の ClassCastException"
},
{
"id": "pmd/CloneMethodMustBePublic",
"description": "clone() must be public if Cloneable"
"description": "clone() must be public if Cloneable",
"descriptionZh": "实现 Cloneable 时 clone() 必须是 public",
"descriptionJa": "Cloneable の場合 clone() は public でなければならない"
},
{
"id": "pmd/CloneMethodMustImplementCloneable",
"description": "clone() only if Cloneable"
"description": "clone() only if Cloneable",
"descriptionZh": "只有实现 Cloneable 时才有 clone()",
"descriptionJa": "Cloneable の場合のみ clone() を持つ"
},
{
"id": "pmd/CloneMethodReturnTypeMustMatchClassName",
"description": "clone() covariant return type"
"description": "clone() covariant return type",
"descriptionZh": "clone() 协变返回类型",
"descriptionJa": "clone() の共変戻り値型"
},
{
"id": "pmd/CloseResource",
"description": "Ensure resources are closed"
"description": "Ensure resources are closed",
"descriptionZh": "确保资源被关闭",
"descriptionJa": "リソースが閉じられることを保証する"
},
{
"id": "pmd/CollectionTypeMismatch",
"description": "Type mismatch in collection methods"
"description": "Type mismatch in collection methods",
"descriptionZh": "集合方法中的类型不匹配",
"descriptionJa": "コレクションメソッド内の型不一致"
},
{
"id": "pmd/CompareObjectsWithEquals",
"description": "Use equals() not == for objects"
"description": "Use equals() not == for objects",
"descriptionZh": "对象比较使用 equals() 而非 ==",
"descriptionJa": "オブジェクトの比較に == ではなく equals() を使用する"
},
{
"id": "pmd/ComparisonWithNaN",
"description": "NaN comparisons always return false"
"description": "NaN comparisons always return false",
"descriptionZh": "NaN 比较总是返回 false",
"descriptionJa": "NaN との比較は常に false を返す"
},
{
"id": "pmd/ConfusingArgumentToVarargsMethod",
"description": "Clarify varargs intent"
"description": "Clarify varargs intent",
"descriptionZh": "澄清可变参数意图",
"descriptionJa": "可変長引数の意図を明確にする"
},
{
"id": "pmd/ConstructorCallsOverridableMethod",
"description": "Constructor calls overridable method"
"description": "Constructor calls overridable method",
"descriptionZh": "构造函数调用可重写方法",
"descriptionJa": "コンストラクタがオーバーライド可能なメソッドを呼ぶ"
},
{
"id": "pmd/DataflowAnomalyAnalysis",
"description": "Data flow anomalies"
"description": "Data flow anomalies",
"descriptionZh": "数据流异常",
"descriptionJa": "データフロー異常"
},
{
"id": "pmd/DoNotCallGarbageCollectionExplicitly",
"description": "Don't call System.gc()"
"description": "Don't call System.gc()",
"descriptionZh": "不要显式调用 System.gc()",
"descriptionJa": "System.gc() を明示的に呼ばない"
},
{
"id": "pmd/DoNotCallSystemExit",
"description": "Don't call System.exit()"
"description": "Don't call System.exit()",
"descriptionZh": "不要调用 System.exit()",
"descriptionJa": "System.exit() を呼ばない"
},
{
"id": "pmd/DoNotHardCodeSDCard",
"description": "Don't hardcode /sdcard path"
"description": "Don't hardcode /sdcard path",
"descriptionZh": "不要硬编码 /sdcard 路径",
"descriptionJa": "/sdcard パスをハードコードしない"
},
{
"id": "pmd/DoNotThrowExceptionInFinally",
"description": "Don't throw in finally"
"description": "Don't throw in finally",
"descriptionZh": "不要在 finally 中抛出异常",
"descriptionJa": "finally で例外を投げない"
},
{
"id": "pmd/DoNotUseThreads",
"description": "Don't use Threads"
"description": "Don't use Threads",
"descriptionZh": "不要使用线程",
"descriptionJa": "スレッドを使用しない"
},
{
"id": "pmd/DontCallThreadRun",
"description": "Don't call Thread.run()"
"description": "Don't call Thread.run()",
"descriptionZh": "不要调用 Thread.run()",
"descriptionJa": "Thread.run() を呼ばない"
},
{
"id": "pmd/DoubleCheckedLocking",
"description": "Double-checked locking is not thread-safe"
"description": "Double-checked locking is not thread-safe",
"descriptionZh": "双重检查锁定不是线程安全的",
"descriptionJa": "二重チェックロッキングはスレッド安全でない"
},
{
"id": "pmd/EmptyCatchBlock",
"description": "Empty catch blocks"
"description": "Empty catch blocks",
"descriptionZh": "空 catch 块",
"descriptionJa": "空の catch ブロック"
},
{
"id": "pmd/EqualsNull",
"description": "Equal comparison to null"
"description": "Equal comparison to null",
"descriptionZh": "与 null 进行相等比较",
"descriptionJa": "null との等価比較"
},
{
"id": "pmd/FinallyBlockDoesNothing",
"description": "Finally block does nothing"
"description": "Finally block does nothing",
"descriptionZh": "finally 块什么也不做",
"descriptionJa": "finally ブロックが何もしない"
},
{
"id": "pmd/IdempotentOperations",
"description": "Idempotent operations"
"description": "Idempotent operations",
"descriptionZh": "幂等操作",
"descriptionJa": "冪等な操作"
},
{
"id": "pmd/ImplicitSwitchFallThrough",
"description": "Implicit switch fall through"
"description": "Implicit switch fall through",
"descriptionZh": "隐式 switch fall through",
"descriptionJa": "暗黙の switch フォールスルー"
},
{
"id": "pmd/ImportFromSamePackage",
"description": "Import from same package"
"description": "Import from same package",
"descriptionZh": "从同包导入",
"descriptionJa": "同一パッケージからのインポート"
},
{
"id": "pmd/InstantiationToGetClass",
"description": "Instantiation just to get class"
"description": "Instantiation just to get class",
"descriptionZh": "仅为获取类而实例化",
"descriptionJa": "クラス取得のためだけのインスタンス化"
},
{
"id": "pmd/InvalidLogMessageFormat",
"description": "Invalid SLF4J message format"
"description": "Invalid SLF4J message format",
"descriptionZh": "无效的 SLF4J 消息格式",
"descriptionJa": "無効な SLF4J メッセージ形式"
},
{
"id": "pmd/JUnitSpelling",
"description": "JUnit method spelling"
"description": "JUnit method spelling",
"descriptionZh": "JUnit 方法拼写",
"descriptionJa": "JUnit メソッドの綴り"
},
{
"id": "pmd/JUnitStaticSuite",
"description": "JUnit static suite method"
"description": "JUnit static suite method",
"descriptionZh": "JUnit 静态 suite 方法",
"descriptionJa": "JUnit の静的 suite メソッド"
},
{
"id": "pmd/JumbledIncrementer",
"description": "Jumbled incrementer"
"description": "Jumbled incrementer",
"descriptionZh": "混乱的增量器",
"descriptionJa": "入り混じったインクリメンタ"
},
{
"id": "pmd/LoggerIsNotStaticFinal",
"description": "Logger not static final"
"description": "Logger not static final",
"descriptionZh": "Logger 不是 static final",
"descriptionJa": "Logger が static final でない"
},
{
"id": "pmd/MethodWithSameNameAsEnclosingClass",
"description": "Method same name as enclosing class"
"description": "Method same name as enclosing class",
"descriptionZh": "方法与包围类同名",
"descriptionJa": "メソッドが囲むクラスと同名"
},
{
"id": "pmd/MisplacedNullCheck",
"description": "Misplaced null check"
"description": "Misplaced null check",
"descriptionZh": "位置错误的 null 检查",
"descriptionJa": "誤った位置の null チェック"
},
{
"id": "pmd/MissingBreakInSwitch",
"description": "Missing break in switch"
"description": "Missing break in switch",
"descriptionZh": "switch 中缺少 break",
"descriptionJa": "switch 内の break 欠落"
},
{
"id": "pmd/MissingSerialVersionUID",
"description": "Missing serialVersionUID"
"description": "Missing serialVersionUID",
"descriptionZh": "缺少 serialVersionUID",
"descriptionJa": "serialVersionUID の欠落"
},
{
"id": "pmd/MissingStaticMethodInNonInstantiatableClass",
"description": "Non-instantiatable class missing static method"
"description": "Non-instantiatable class missing static method",
"descriptionZh": "不可实例化类缺少静态方法",
"descriptionJa": "インスタンス化できないクラスに静的メソッドがない"
},
{
"id": "pmd/MoreThanOneLogger",
"description": "More than one logger"
"description": "More than one logger",
"descriptionZh": "多于一个 logger",
"descriptionJa": "logger が複数ある"
},
{
"id": "pmd/NonCaseLabelInSwitch",
"description": "Non-case label in switch"
"description": "Non-case label in switch",
"descriptionZh": "switch 中的非 case 标签",
"descriptionJa": "switch 内の非 case ラベル"
},
{
"id": "pmd/NonStaticInitializer",
"description": "Non-static initializer"
"description": "Non-static initializer",
"descriptionZh": "非静态初始化器",
"descriptionJa": "非静的初期化子"
},
{
"id": "pmd/NonThreadSafeSingleton",
"description": "Singleton is not thread-safe"
"description": "Singleton is not thread-safe",
"descriptionZh": "单例不是线程安全的",
"descriptionJa": "シングルトンがスレッド安全でない"
},
{
"id": "pmd/NullAssignment",
"description": "Null assignment"
"description": "Null assignment",
"descriptionZh": "null 赋值",
"descriptionJa": "null 代入"
},
{
"id": "pmd/NumberConstructor",
"description": "Number constructor (deprecated)"
"description": "Number constructor (deprecated)",
"descriptionZh": "Number 构造函数(已废弃)",
"descriptionJa": "Number コンストラクタ(非推奨)"
},
{
"id": "pmd/ObjectFinalize",
"description": "Object finalize issues"
"description": "Object finalize issues",
"descriptionZh": "Object finalize 问题",
"descriptionJa": "Object finalize の問題"
},
{
"id": "pmd/OperationWithCloning",
"description": "Operation with cloning"
"description": "Operation with cloning",
"descriptionZh": "克隆操作",
"descriptionJa": "クローン操作"
},
{
"id": "pmd/OverrideBothEqualsAndHashcode",
"description": "Override both equals() and hashCode()"
"description": "Override both equals() and hashCode()",
"descriptionZh": "同时重写 equals() 和 hashCode()",
"descriptionJa": "equals() と hashCode() の両方をオーバーライドする"
},
{
"id": "pmd/OverridingThreadRun",
"description": "Don't override Thread.run()"
"description": "Don't override Thread.run()",
"descriptionZh": "不要重写 Thread.run()",
"descriptionJa": "Thread.run() をオーバーライドしない"
},
{
"id": "pmd/PackageDeclaration",
"description": "Package declaration"
"description": "Package declaration",
"descriptionZh": "包声明",
"descriptionJa": "パッケージ宣言"
},
{
"id": "pmd/ProperCloneImplementation",
"description": "Proper clone implementation"
"description": "Proper clone implementation",
"descriptionZh": "正确的 clone 实现",
"descriptionJa": "適切な clone 実装"
},
{
"id": "pmd/ProperLogger",
"description": "Proper logger"
"description": "Proper logger",
"descriptionZh": "正确的 logger",
"descriptionJa": "適切な logger"
},
{
"id": "pmd/ReturnFromFinallyBlock",
"description": "Return from finally"
"description": "Return from finally",
"descriptionZh": "从 finally 返回",
"descriptionJa": "finally からの return"
},
{
"id": "pmd/SimpleDateFormatNeedsLocale",
"description": "SimpleDateFormat needs locale"
"description": "SimpleDateFormat needs locale",
"descriptionZh": "SimpleDateFormat 需要 locale",
"descriptionJa": "SimpleDateFormat に locale が必要"
},
{
"id": "pmd/SingleMethodSingleton",
"description": "Singleton pattern issues"
"description": "Singleton pattern issues",
"descriptionZh": "单例模式问题",
"descriptionJa": "シングルトンパターンの問題"
},
{
"id": "pmd/SingletonClassReturningNewInstance",
"description": "Singleton returning new instance"
"description": "Singleton returning new instance",
"descriptionZh": "单例返回新实例",
"descriptionJa": "シングルトンが新しいインスタンスを返す"
},
{
"id": "pmd/StaticEJBFieldShouldBeFinal",
"description": "Static EJB field should be final"
"description": "Static EJB field should be final",
"descriptionZh": "静态 EJB 字段应为 final",
"descriptionJa": "静的 EJB フィールドは final にする"
},
{
"id": "pmd/StringBufferInstantiationWithChar",
"description": "StringBuffer with char"
"description": "StringBuffer with char",
"descriptionZh": "StringBuffer 带 char 实例化",
"descriptionJa": "char を伴う StringBuffer インスタンス化"
},
{
"id": "pmd/SuspiciousConstantFieldName",
"description": "Constant field naming"
"description": "Constant field naming",
"descriptionZh": "常量字段命名",
"descriptionJa": "定数フィールドの命名"
},
{
"id": "pmd/SuspiciousEqualsMethodName",
"description": "equals() method signature"
"description": "equals() method signature",
"descriptionZh": "equals() 方法签名",
"descriptionJa": "equals() メソッドのシグネチャ"
},
{
"id": "pmd/SuspiciousHashcodeMethodName",
"description": "hashCode() method signature"
"description": "hashCode() method signature",
"descriptionZh": "hashCode() 方法签名",
"descriptionJa": "hashCode() メソッドのシグネチャ"
},
{
"id": "pmd/SuspiciousOctalEscape",
"description": "Suspicious octal escape"
"description": "Suspicious octal escape",
"descriptionZh": "可疑的八进制转义",
"descriptionJa": "疑わしい8進エスケープ"
},
{
"id": "pmd/TestClassWithoutTestCases",
"description": "Test class without test cases"
"description": "Test class without test cases",
"descriptionZh": "没有测试用例的测试类",
"descriptionJa": "テストケースのないテストクラス"
},
{
"id": "pmd/UnconditionalIfStatement",
"description": "Unconditional if statement"
"description": "Unconditional if statement",
"descriptionZh": "无条件 if 语句",
"descriptionJa": "無条件の if 文"
},
{
"id": "pmd/UnnecessaryBooleanAssertion",
"description": "Unnecessary boolean assertion"
"description": "Unnecessary boolean assertion",
"descriptionZh": "不必要的布尔断言",
"descriptionJa": "不要なブールアサーション"
},
{
"id": "pmd/UnnecessaryCaseChange",
"description": "Unnecessary case change"
"description": "Unnecessary case change",
"descriptionZh": "不必要的大小写转换",
"descriptionJa": "不要なケース変換"
},
{
"id": "pmd/UnnecessaryConversionTemporal",
"description": "Unnecessary temporal conversion"
"description": "Unnecessary temporal conversion",
"descriptionZh": "不必要的时间转换",
"descriptionJa": "不要な時間変換"
},
{
"id": "pmd/UnusedNullCheckInEquals",
"description": "Unused null check in equals"
"description": "Unused null check in equals",
"descriptionZh": "equals 中未使用的 null 检查",
"descriptionJa": "equals 内の未使用 null チェック"
},
{
"id": "pmd/UseConcurrentHashMap",
"description": "Use ConcurrentHashMap for concurrent access"
"description": "Use ConcurrentHashMap for concurrent access",
"descriptionZh": "并发访问使用 ConcurrentHashMap",
"descriptionJa": "並行アクセスに ConcurrentHashMap を使用する"
},
{
"id": "pmd/UseCorrectExceptionLogging",
"description": "Correct exception logging"
"description": "Correct exception logging",
"descriptionZh": "正确的异常日志",
"descriptionJa": "適切な例外ログ"
},
{
"id": "pmd/UseDiamondOperator",
"description": "Use diamond operator <>"
"description": "Use diamond operator <>",
"descriptionZh": "使用菱形运算符 <>",
"descriptionJa": "ダイヤモンド演算子 <> を使用する"
},
{
"id": "pmd/UseEqualsToCompareStrings",
"description": "Use equals() for strings"
"description": "Use equals() for strings",
"descriptionZh": "字符串比较使用 equals()",
"descriptionJa": "文字列の比較に equals() を使用する"
},
{
"id": "pmd/UseLocaleWithCaseConversions",
"description": "Use locale with case conversions"
"description": "Use locale with case conversions",
"descriptionZh": "大小写转换使用 locale",
"descriptionJa": "ケース変換に locale を使用する"
},
{
"id": "pmd/UseNotifyAllInsteadOfNotify",
"description": "Use notifyAll() instead of notify()"
"description": "Use notifyAll() instead of notify()",
"descriptionZh": "使用 notifyAll() 替代 notify()",
"descriptionJa": "notify() の代わりに notifyAll() を使用する"
},
{
"id": "pmd/UseProperClassLoader",
"description": "Use proper classloader"
"description": "Use proper classloader",
"descriptionZh": "使用正确的类加载器",
"descriptionJa": "適切なクラスローダーを使用する"
},
{
"id": "pmd/AddEmptyString",
"description": "Don't add empty strings"
"description": "Don't add empty strings",
"descriptionZh": "不要添加空字符串",
"descriptionJa": "空文字列を追加しない"
},
{
"id": "pmd/AppendCharacterWithChar",
"description": "Append char not string in StringBuffer"
"description": "Append char not string in StringBuffer",
"descriptionZh": "StringBuffer 中追加 char 而非 String",
"descriptionJa": "StringBuffer には String ではなく char を追加する"
},
{
"id": "pmd/AvoidArrayLoops",
"description": "Use Arrays.copyOf or System.arraycopy"
"description": "Use Arrays.copyOf or System.arraycopy",
"descriptionZh": "使用 Arrays.copyOf 或 System.arraycopy",
"descriptionJa": "Arrays.copyOf または System.arraycopy を使用する"
},
{
"id": "pmd/AvoidCalendarDateCreation",
"description": "Avoid Calendar for current time"
"description": "Avoid Calendar for current time",
"descriptionZh": "获取当前时间避免使用 Calendar",
"descriptionJa": "現在時刻に Calendar を使用しない"
},
{
"id": "pmd/AvoidFileStream",
"description": "Avoid FileInputStream/FileOutputStream/FileReader/FileWriter"
"description": "Avoid FileInputStream/FileOutputStream/FileReader/FileWriter",
"descriptionZh": "避免 FileInputStream/FileOutputStream/FileReader/FileWriter",
"descriptionJa": "FileInputStream/FileOutputStream/FileReader/FileWriter を避ける"
},
{
"id": "pmd/AvoidInstantiatingObjectsInLoops",
"description": "Don't instantiate objects in loops"
"description": "Don't instantiate objects in loops",
"descriptionZh": "不要在循环中实例化对象",
"descriptionJa": "ループ内でオブジェクトをインスタンス化しない"
},
{
"id": "pmd/BigIntegerInstantiation",
"description": "Use BigInteger.ZERO/ONE/TEN"
"description": "Use BigInteger.ZERO/ONE/TEN",
"descriptionZh": "使用 BigInteger.ZERO/ONE/TEN",
"descriptionJa": "BigInteger.ZERO/ONE/TEN を使用する"
},
{
"id": "pmd/ConsecutiveAppendsShouldReuse",
"description": "Chain StringBuilder.append calls"
"description": "Chain StringBuilder.append calls",
"descriptionZh": "链式调用 StringBuilder.append",
"descriptionJa": "StringBuilder.append をチェーンで呼ぶ"
},
{
"id": "pmd/ConsecutiveLiteralAppends",
"description": "Combine literal appends"
"description": "Combine literal appends",
"descriptionZh": "合并字面量追加",
"descriptionJa": "リテラルの追加を統合する"
},
{
"id": "pmd/InefficientEmptyStringCheck",
"description": "Use isBlank() instead of trim().isEmpty()"
"description": "Use isBlank() instead of trim().isEmpty()",
"descriptionZh": "使用 isBlank() 替代 trim().isEmpty()",
"descriptionJa": "trim().isEmpty() の代わりに isBlank() を使用する"
},
{
"id": "pmd/InefficientStringBuffering",
"description": "Avoid concatenating in StringBuffer constructor"
"description": "Avoid concatenating in StringBuffer constructor",
"descriptionZh": "避免在 StringBuffer 构造函数中拼接",
"descriptionJa": "StringBuffer コンストラクタ内での連結を避ける"
},
{
"id": "pmd/InsufficientStringBufferDeclaration",
"description": "Pre-size StringBuilder"
"description": "Pre-size StringBuilder",
"descriptionZh": "预先指定 StringBuilder 容量",
"descriptionJa": "StringBuilder の容量を事前指定する"
},
{
"id": "pmd/OptimizableToArrayCall",
"description": "Use new Foo[0] instead of new Foo[size]"
"description": "Use new Foo[0] instead of new Foo[size]",
"descriptionZh": "使用 new Foo[0] 替代 new Foo[size]",
"descriptionJa": "new Foo[size] の代わりに new Foo[0] を使用する"
},
{
"id": "pmd/RedundantFieldInitializer",
"description": "Remove redundant field initializers"
"description": "Remove redundant field initializers",
"descriptionZh": "移除冗余的字段初始化器",
"descriptionJa": "冗長なフィールド初期化子を削除する"
},
{
"id": "pmd/StringInstantiation",
"description": "Avoid new String()"
"description": "Avoid new String()",
"descriptionZh": "避免 new String()",
"descriptionJa": "new String() を避ける"
},
{
"id": "pmd/StringToString",
"description": "Avoid toString() on String"
"description": "Avoid toString() on String",
"descriptionZh": "避免对 String 调用 toString()",
"descriptionJa": "String への toString() を避ける"
},
{
"id": "pmd/TooFewBranchesForSwitch",
"description": "Switch with less than 3 branches"
"description": "Switch with less than 3 branches",
"descriptionZh": "少于3个分支的 switch",
"descriptionJa": "3未満のブランチの switch"
},
{
"id": "pmd/UseArrayListInsteadOfVector",
"description": "ArrayList instead of Vector"
"description": "ArrayList instead of Vector",
"descriptionZh": "使用 ArrayList 替代 Vector",
"descriptionJa": "Vector の代わりに ArrayList を使用する"
},
{
"id": "pmd/UseArraysAsList",
"description": "Use Arrays.asList() instead of loop"
"description": "Use Arrays.asList() instead of loop",
"descriptionZh": "使用 Arrays.asList() 替代循环",
"descriptionJa": "ループの代わりに Arrays.asList() を使用する"
},
{
"id": "pmd/UseIndexOfChar",
"description": "Use indexOf(char) not indexOf(String)"
"description": "Use indexOf(char) not indexOf(String)",
"descriptionZh": "使用 indexOf(char) 而非 indexOf(String)",
"descriptionJa": "indexOf(String) ではなく indexOf(char) を使用する"
},
{
"id": "pmd/UseIOStreamsWithApacheCommonsFileItem",
"description": "Use getInputStream() not get()"
"description": "Use getInputStream() not get()",
"descriptionZh": "使用 getInputStream() 而非 get()",
"descriptionJa": "get() ではなく getInputStream() を使用する"
},
{
"id": "pmd/UselessStringValueOf",
"description": "Don't wrap with String.valueOf()"
"description": "Don't wrap with String.valueOf()",
"descriptionZh": "不要用 String.valueOf() 包裹",
"descriptionJa": "String.valueOf() でラップしない"
},
{
"id": "pmd/UseStringBufferForStringAppends",
"description": "Use StringBuilder for concatenation"
"description": "Use StringBuilder for concatenation",
"descriptionZh": "使用 StringBuilder 进行拼接",
"descriptionJa": "文字列連結に StringBuilder を使用する"
},
{
"id": "pmd/UseStringBufferLength",
"description": "Use length() instead of toString().equals(\"\")"
"description": "Use length() instead of toString().equals(\"\")",
"descriptionZh": "使用 length() 替代 toString().equals(\"\")",
"descriptionJa": "toString().equals(\"\") の代わりに length() を使用する"
},
{
"id": "pmd/HardCodedCryptoKey",
"description": "Don't hard code encryption keys"
"description": "Don't hard code encryption keys",
"descriptionZh": "不要硬编码加密密钥",
"descriptionJa": "暗号鍵をハードコードしない"
},
{
"id": "pmd/InsecureCryptoIv",
"description": "Don't hard code initialization vectors"
"description": "Don't hard code initialization vectors",
"descriptionZh": "不要硬编码初始化向量",
"descriptionJa": "初期化ベクタをハードコードしない"
}
],
"pmd-jsp": [
{
"id": "pmd-jsp/DontNestJsfInJstlIteration",
"description": "Do not nest JSF components inside JSTL iteration"
"description": "Do not nest JSF components inside JSTL iteration",
"descriptionZh": "不要在 JSTL 迭代内嵌套 JSF 组件",
"descriptionJa": "JSTL 反復内に JSF コンポーネントをネストしない"
},
{
"id": "pmd-jsp/NoClassAttribute",
"description": "Use styleclass not class attribute"
"description": "Use styleclass not class attribute",
"descriptionZh": "使用 styleclass 而非 class 属性",
"descriptionJa": "class 属性ではなく styleclass を使用する"
},
{
"id": "pmd-jsp/NoHtmlComments",
"description": "Use JSP comments instead of HTML comments"
"description": "Use JSP comments instead of HTML comments",
"descriptionZh": "使用 JSP 注释而非 HTML 注释",
"descriptionJa": "HTML コメントではなく JSP コメントを使用する"
},
{
"id": "pmd-jsp/NoJspForward",
"description": "Do not forward from within a JSP"
"description": "Do not forward from within a JSP",
"descriptionZh": "不要在 JSP 内转发",
"descriptionJa": "JSP 内からフォワードしない"
},
{
"id": "pmd-jsp/DuplicateJspImports",
"description": "Avoid duplicate imports in JSP"
"description": "Avoid duplicate imports in JSP",
"descriptionZh": "避免 JSP 中重复导入",
"descriptionJa": "JSP 内の重複インポートを避ける"
},
{
"id": "pmd-jsp/NoInlineScript",
"description": "Externalize HTML script content"
"description": "Externalize HTML script content",
"descriptionZh": "将 HTML 脚本内容外部化",
"descriptionJa": "HTML スクリプト内容を外部化する"
},
{
"id": "pmd-jsp/NoInlineStyleInformation",
"description": "Put styles in CSS files"
"description": "Put styles in CSS files",
"descriptionZh": "将样式放入 CSS 文件",
"descriptionJa": "スタイルを CSS ファイルに置く"
},
{
"id": "pmd-jsp/NoLongScripts",
"description": "Avoid long scripts in JSP"
"description": "Avoid long scripts in JSP",
"descriptionZh": "避免 JSP 中的长脚本",
"descriptionJa": "JSP 内の長いスクリプトを避ける"
},
{
"id": "pmd-jsp/NoScriptlets",
"description": "Avoid scriptlets in JSP"
"description": "Avoid scriptlets in JSP",
"descriptionZh": "避免 JSP 中的 scriptlet",
"descriptionJa": "JSP 内のスクリプトレットを避ける"
},
{
"id": "pmd-jsp/JspEncoding",
"description": "JSP files should use UTF-8 encoding"
"description": "JSP files should use UTF-8 encoding",
"descriptionZh": "JSP 文件应使用 UTF-8 编码",
"descriptionJa": "JSP ファイルは UTF-8 エンコーディングを使用すべきである"
}
],
"sql-lint": [
"sqlfluff": [
{
"id": "sql-lint/AL01",
"id": "sqlfluff/AL01",
"description": "Implicit/explicit aliasing of table",
"tier": "P2"
"tier": "P2",
"descriptionZh": "表的隐式/显式别名",
"descriptionJa": "テーブルの暗黙的/明示的エイリアス"
},
{
"id": "sql-lint/AL02",
"id": "sqlfluff/AL02",
"description": "Implicit/explicit aliasing of columns",
"tier": "P0"
"tier": "P0",
"descriptionZh": "列的隐式/显式别名",
"descriptionJa": "列の暗黙的/明示的エイリアス"
},
{
"id": "sql-lint/AL03",
"id": "sqlfluff/AL03",
"description": "Column expression without alias",
"tier": "P0"
"tier": "P0",
"descriptionZh": "无别名的列表达式",
"descriptionJa": "エイリアスなしの列式"
},
{
"id": "sql-lint/AL04",
"id": "sqlfluff/AL04",
"description": "Table aliases should be unique within each clause",
"tier": "P0"
"tier": "P0",
"descriptionZh": "表别名在每个子句中应唯一",
"descriptionJa": "表エイリアスは各句内で一意にすべきである"
},
{
"id": "sql-lint/AL05",
"id": "sqlfluff/AL05",
"description": "Tables should not be aliased if unused",
"tier": "P0"
"tier": "P0",
"descriptionZh": "未使用的表不应加别名",
"descriptionJa": "未使用のテーブルにエイリアスを付けない"
},
{
"id": "sql-lint/AL06",
"id": "sqlfluff/AL06",
"description": "Enforce table alias lengths",
"tier": "P0"
"tier": "P0",
"descriptionZh": "强制表别名长度",
"descriptionJa": "表エイリアスの長さを強制する"
},
{
"id": "sql-lint/AL07",
"id": "sqlfluff/AL07",
"description": "Avoid table aliases",
"tier": "excluded"
"tier": "excluded",
"descriptionZh": "避免表别名",
"descriptionJa": "表エイリアスを避ける"
},
{
"id": "sql-lint/AL08",
"id": "sqlfluff/AL08",
"description": "Column aliases should be unique within each clause",
"tier": "P0"
"tier": "P0",
"descriptionZh": "列别名在每个子句中应唯一",
"descriptionJa": "列エイリアスは各句内で一意にすべきである"
},
{
"id": "sql-lint/AL09",
"id": "sqlfluff/AL09",
"description": "Column aliases should not alias to itself",
"tier": "P0"
"tier": "P0",
"descriptionZh": "列别名不应与自身相同",
"descriptionJa": "列エイリアスが自分自身と同じにならないようにする"
},
{
"id": "sql-lint/AL10",
"id": "sqlfluff/AL10",
"description": "Derived tables must have an alias",
"tier": "P0"
"tier": "P0",
"descriptionZh": "派生表必须使用别名",
"descriptionJa": "派生テーブルにはエイリアスが必要である"
},
{
"id": "sql-lint/AM01",
"id": "sqlfluff/AM01",
"description": "Ambiguous use of DISTINCT with GROUP BY",
"tier": "P0"
"tier": "P0",
"descriptionZh": "DISTINCT 与 GROUP BY 的歧义用法",
"descriptionJa": "DISTINCT と GROUP BY の曖昧な使用"
},
{
"id": "sql-lint/AM02",
"id": "sqlfluff/AM02",
"description": "UNION DISTINCT/ALL preferred over just UNION",
"tier": "P0"
"tier": "P0",
"descriptionZh": "优先使用 UNION DISTINCT/ALL 而非仅 UNION",
"descriptionJa": "単なる UNION より UNION DISTINCT/ALL を優先する"
},
{
"id": "sql-lint/AM03",
"id": "sqlfluff/AM03",
"description": "Ambiguous ordering directions",
"tier": "P1"
"tier": "P1",
"descriptionZh": "歧义的排序方向",
"descriptionJa": "曖昧なソート方向"
},
{
"id": "sql-lint/AM04",
"id": "sqlfluff/AM04",
"description": "Query produces unknown number of result columns",
"tier": "P2"
"tier": "P2",
"descriptionZh": "查询产生未知数量的结果列",
"descriptionJa": "クエリが未知数の結果列を生成する"
},
{
"id": "sql-lint/AM05",
"id": "sqlfluff/AM05",
"description": "Join clauses should be fully qualified",
"tier": "P1"
"tier": "P1",
"descriptionZh": "连接子句应完全限定",
"descriptionJa": "結合句は完全修飾すべきである"
},
{
"id": "sql-lint/AM06",
"id": "sqlfluff/AM06",
"description": "Inconsistent column references in GROUP BY/ORDER BY",
"tier": "P0"
"tier": "P0",
"descriptionZh": "GROUP BY/ORDER BY 中列引用不一致",
"descriptionJa": "GROUP BY/ORDER BY 内の列参照の不整合"
},
{
"id": "sql-lint/AM07",
"id": "sqlfluff/AM07",
"description": "Queries within set query produce different numbers of columns",
"tier": "P2"
"tier": "P2",
"descriptionZh": "集合查询中的子查询产生不同数量的列",
"descriptionJa": "集合クエリ内のサブクエリが異なる数の列を生成する"
},
{
"id": "sql-lint/AM08",
"id": "sqlfluff/AM08",
"description": "Implicit cross join detected",
"tier": "P1"
"tier": "P1",
"descriptionZh": "检测到隐式交叉连接",
"descriptionJa": "暗黙のクロスジョインを検出"
},
{
"id": "sql-lint/AM09",
"id": "sqlfluff/AM09",
"description": "LIMIT/OFFSET without ORDER BY non-deterministic",
"tier": "P2"
"tier": "P2",
"descriptionZh": "无 ORDER BY 的 LIMIT/OFFSET 是非确定性的",
"descriptionJa": "ORDER BY なしの LIMIT/OFFSET は非決定的である"
},
{
"id": "sql-lint/CP01",
"id": "sqlfluff/CP01",
"description": "Inconsistent capitalisation of keywords",
"tier": "P0"
"tier": "P0",
"descriptionZh": "关键字大小写不一致",
"descriptionJa": "キーワードの大文字小文字が不統一"
},
{
"id": "sql-lint/CP02",
"id": "sqlfluff/CP02",
"description": "Inconsistent capitalisation of unquoted identifiers",
"tier": "P0"
"tier": "P0",
"descriptionZh": "未加引号的标识符大小写不一致",
"descriptionJa": "引用なし識別子の大文字小文字が不統一"
},
{
"id": "sql-lint/CP03",
"id": "sqlfluff/CP03",
"description": "Inconsistent capitalisation of function names",
"tier": "P0"
"tier": "P0",
"descriptionZh": "函数名大小写不一致",
"descriptionJa": "関数名の大文字小文字が不統一"
},
{
"id": "sql-lint/CP04",
"id": "sqlfluff/CP04",
"description": "Inconsistent capitalisation of boolean/null literal",
"tier": "P0"
"tier": "P0",
"descriptionZh": "布尔/null 字面量大小写不一致",
"descriptionJa": "ブール/null リテラルの大文字小文字が不統一"
},
{
"id": "sql-lint/CP05",
"id": "sqlfluff/CP05",
"description": "Inconsistent capitalisation of datatypes",
"tier": "P0"
"tier": "P0",
"descriptionZh": "数据类型大小写不一致",
"descriptionJa": "データ型の大文字小文字が不統一"
},
{
"id": "sql-lint/CV01",
"id": "sqlfluff/CV01",
"description": "Consistent usage of != or <>",
"tier": "P1"
"tier": "P1",
"descriptionZh": "一致地使用 != 或 <>",
"descriptionJa": "!= または <> を一貫して使用する"
},
{
"id": "sql-lint/CV02",
"id": "sqlfluff/CV02",
"description": "Use COALESCE instead of IFNULL/NVL",
"tier": "P1"
"tier": "P1",
"descriptionZh": "使用 COALESCE 替代 IFNULL/NVL",
"descriptionJa": "IFNULL/NVL の代わりに COALESCE を使用する"
},
{
"id": "sql-lint/CV03",
"id": "sqlfluff/CV03",
"description": "Trailing commas within select clause",
"tier": "P0"
"tier": "P0",
"descriptionZh": "select 子句中的尾随逗号",
"descriptionJa": "select 句内の末尾カンマ"
},
{
"id": "sql-lint/CV04",
"id": "sqlfluff/CV04",
"description": "Consistent syntax for count number of rows",
"tier": "P0"
"tier": "P0",
"descriptionZh": "计数行数的一致语法",
"descriptionJa": "行数を数える一貫した構文"
},
{
"id": "sql-lint/CV05",
"id": "sqlfluff/CV05",
"description": "Comparisons with NULL should use IS or IS NOT",
"tier": "P0"
"tier": "P0",
"descriptionZh": "与 NULL 比较应使用 IS 或 IS NOT",
"descriptionJa": "NULL との比較には IS または IS NOT を使用する"
},
{
"id": "sql-lint/CV06",
"id": "sqlfluff/CV06",
"description": "Statements must end with a semi-colon",
"tier": "P1"
"tier": "P1",
"descriptionZh": "语句必须以分号结尾",
"descriptionJa": "文はセミコロンで終わらせる"
},
{
"id": "sql-lint/CV07",
"id": "sqlfluff/CV07",
"description": "Top-level statements should not be wrapped in brackets",
"tier": "P2"
"tier": "P2",
"descriptionZh": "顶层语句不应包裹在括号中",
"descriptionJa": "トップレベルの文を括弧で囲まない"
},
{
"id": "sql-lint/CV08",
"id": "sqlfluff/CV08",
"description": "Use LEFT JOIN instead of RIGHT JOIN",
"tier": "P1"
"tier": "P1",
"descriptionZh": "使用 LEFT JOIN 替代 RIGHT JOIN",
"descriptionJa": "RIGHT JOIN の代わりに LEFT JOIN を使用する"
},
{
"id": "sql-lint/CV09",
"id": "sqlfluff/CV09",
"description": "Block a list of configurable words",
"tier": "excluded"
"tier": "excluded",
"descriptionZh": "屏蔽一组可配置的词",
"descriptionJa": "設定可能な語のリストをブロックする"
},
{
"id": "sql-lint/CV10",
"id": "sqlfluff/CV10",
"description": "Consistent usage of preferred quotes for quoted literals",
"tier": "excluded"
"tier": "excluded",
"descriptionZh": "引用的字面量一致使用首选引号",
"descriptionJa": "引用リテラルに推奨引用符を一貫して使用する"
},
{
"id": "sql-lint/CV11",
"id": "sqlfluff/CV11",
"description": "Enforce consistent type casting style",
"tier": "P2"
"tier": "P2",
"descriptionZh": "强制一致的类型转换风格",
"descriptionJa": "一貫した型変換スタイルを強制する"
},
{
"id": "sql-lint/CV12",
"id": "sqlfluff/CV12",
"description": "Use JOIN ... ON ... instead of WHERE ... for join conditions",
"tier": "P1"
"tier": "P1",
"descriptionZh": "连接条件使用 JOIN ... ON ... 而非 WHERE",
"descriptionJa": "結合条件に WHERE ではなく JOIN ... ON ... を使用する"
},
{
"id": "sql-lint/JJ01",
"id": "sqlfluff/JJ01",
"description": "Jinja tags should have single whitespace on either side",
"tier": "P0"
"tier": "P0",
"descriptionZh": "Jinja 标签两侧应各有一个空格",
"descriptionJa": "Jinja タグの両側に空白を1つ置く"
},
{
"id": "sql-lint/LT01",
"id": "sqlfluff/LT01",
"description": "Inappropriate Spacing",
"tier": "P0"
"tier": "P0",
"descriptionZh": "不合适的间距",
"descriptionJa": "不適切な間隔"
},
{
"id": "sql-lint/LT02",
"id": "sqlfluff/LT02",
"description": "Incorrect Indentation",
"tier": "P0"
"tier": "P0",
"descriptionZh": "不正确的缩进",
"descriptionJa": "不適切なインデント"
},
{
"id": "sql-lint/LT03",
"id": "sqlfluff/LT03",
"description": "Operators before/after newlines",
"tier": "excluded"
"tier": "excluded",
"descriptionZh": "运算符在换行前/后",
"descriptionJa": "改行前後への演算子の配置"
},
{
"id": "sql-lint/LT04",
"id": "sqlfluff/LT04",
"description": "Leading/Trailing comma enforcement",
"tier": "excluded"
"tier": "excluded",
"descriptionZh": "前导/尾随逗号的强制",
"descriptionJa": "先頭/末尾カンマの強制"
},
{
"id": "sql-lint/LT05",
"id": "sqlfluff/LT05",
"description": "Line is too long",
"tier": "P0"
"tier": "P0",
"descriptionZh": "行过长",
"descriptionJa": "行が長すぎる"
},
{
"id": "sql-lint/LT06",
"id": "sqlfluff/LT06",
"description": "Function name not followed by parenthesis",
"tier": "P0"
"tier": "P0",
"descriptionZh": "函数名后未跟括号",
"descriptionJa": "関数名の後に括弧がない"
},
{
"id": "sql-lint/LT07",
"id": "sqlfluff/LT07",
"description": "WITH clause closing bracket on new line",
"tier": "P0"
"tier": "P0",
"descriptionZh": "WITH 子句的右括号应在新行",
"descriptionJa": "WITH 句の閉じ括弧を新しい行に置く"
},
{
"id": "sql-lint/LT08",
"id": "sqlfluff/LT08",
"description": "Blank line after CTE closing bracket",
"tier": "P0"
"tier": "P0",
"descriptionZh": "CTE 右括号后应有空行",
"descriptionJa": "CTE の閉じ括弧の後に空行を置く"
},
{
"id": "sql-lint/LT09",
"id": "sqlfluff/LT09",
"description": "Select targets on new line",
"tier": "excluded"
"tier": "excluded",
"descriptionZh": "select 目标在新行",
"descriptionJa": "select 対象を新しい行に置く"
},
{
"id": "sql-lint/LT10",
"id": "sqlfluff/LT10",
"description": "SELECT modifiers on same line as SELECT",
"tier": "P0"
"tier": "P0",
"descriptionZh": "SELECT 修饰符与 SELECT 同行",
"descriptionJa": "SELECT 修飾子を SELECT と同じ行に置く"
},
{
"id": "sql-lint/LT11",
"id": "sqlfluff/LT11",
"description": "Set operators surrounded by newlines",
"tier": "P0"
"tier": "P0",
"descriptionZh": "集合运算符周围应有换行",
"descriptionJa": "集合演算子を改行で囲む"
},
{
"id": "sql-lint/LT12",
"id": "sqlfluff/LT12",
"description": "Files must end with single trailing newline",
"tier": "P0"
"tier": "P0",
"descriptionZh": "文件必须以单个尾随换行结束",
"descriptionJa": "ファイルは単一の末尾改行で終わる"
},
{
"id": "sql-lint/LT13",
"id": "sqlfluff/LT13",
"description": "Files must not begin with newlines/whitespace",
"tier": "P1"
"tier": "P1",
"descriptionZh": "文件不得以换行/空白开头",
"descriptionJa": "ファイルを改行/空白で始めない"
},
{
"id": "sql-lint/LT14",
"id": "sqlfluff/LT14",
"description": "Keyword clauses before/after newlines",
"tier": "P1"
"tier": "P1",
"descriptionZh": "关键字子句在换行前/后",
"descriptionJa": "キーワード句の改行前後の配置"
},
{
"id": "sql-lint/LT15",
"id": "sqlfluff/LT15",
"description": "Too many consecutive blank lines",
"tier": "P1"
"tier": "P1",
"descriptionZh": "连续空行过多",
"descriptionJa": "連続する空行が多すぎる"
},
{
"id": "sql-lint/OR01",
"id": "sqlfluff/OR01",
"description": "Remove empty batches",
"tier": "P2"
"tier": "P2",
"descriptionZh": "移除空批次",
"descriptionJa": "空のバッチを削除する"
},
{
"id": "sql-lint/PG01",
"id": "sqlfluff/PG01",
"description": "Avoid excessive locks in PostgreSQL DDL",
"tier": "P2"
"tier": "P2",
"descriptionZh": "避免 PostgreSQL DDL 中过度的锁",
"descriptionJa": "PostgreSQL DDL での過度なロックを避ける"
},
{
"id": "sql-lint/RF01",
"id": "sqlfluff/RF01",
"description": "References cannot reference objects not in FROM clause",
"tier": "P0"
"tier": "P0",
"descriptionZh": "引用不能引用 FROM 子句中不存在的对象",
"descriptionJa": "FROM 句にないオブジェクトを参照できない"
},
{
"id": "sql-lint/RF02",
"id": "sqlfluff/RF02",
"description": "References should be qualified if multiple tables",
"tier": "P1"
"tier": "P1",
"descriptionZh": "多表时应限定引用",
"descriptionJa": "複数テーブルの場合は参照を修飾すべきである"
},
{
"id": "sql-lint/RF03",
"id": "sqlfluff/RF03",
"description": "Column references consistent in single table statements",
"tier": "excluded"
"tier": "excluded",
"descriptionZh": "单表语句中列引用一致",
"descriptionJa": "単一テーブル文での列参照の一貫性"
},
{
"id": "sql-lint/RF04",
"id": "sqlfluff/RF04",
"description": "Keywords should not be used as identifiers",
"tier": "P1"
"tier": "P1",
"descriptionZh": "关键字不应用作标识符",
"descriptionJa": "キーワードを識別子として使用しない"
},
{
"id": "sql-lint/RF05",
"id": "sqlfluff/RF05",
"description": "No special characters in identifiers",
"tier": "P1"
"tier": "P1",
"descriptionZh": "标识符中不应有特殊字符",
"descriptionJa": "識別子に特殊文字を含めない"
},
{
"id": "sql-lint/RF06",
"id": "sqlfluff/RF06",
"description": "Unnecessary quoted identifier",
"tier": "P1"
"tier": "P1",
"descriptionZh": "不必要的加引号标识符",
"descriptionJa": "不要な引用付き識別子"
},
{
"id": "sql-lint/ST01",
"id": "sqlfluff/ST01",
"description": "Do not specify else null in CASE WHEN",
"tier": "P1"
"tier": "P1",
"descriptionZh": "不要在 CASE WHEN 中指定 else null",
"descriptionJa": "CASE WHEN で else null を指定しない"
},
{
"id": "sql-lint/ST02",
"id": "sqlfluff/ST02",
"description": "Unnecessary CASE statement",
"tier": "P1"
"tier": "P1",
"descriptionZh": "不必要的 CASE 语句",
"descriptionJa": "不要な CASE 文"
},
{
"id": "sql-lint/ST03",
"id": "sqlfluff/ST03",
"description": "Unused CTE",
"tier": "P0"
"tier": "P0",
"descriptionZh": "未使用的 CTE",
"descriptionJa": "未使用の CTE"
},
{
"id": "sql-lint/ST04",
"id": "sqlfluff/ST04",
"description": "Nested CASE in ELSE clause can be flattened",
"tier": "P1"
"tier": "P1",
"descriptionZh": "ELSE 子句中的嵌套 CASE 可以扁平化",
"descriptionJa": "ELSE 句内のネスト CASE は平坦化できる"
},
{
"id": "sql-lint/ST05",
"id": "sqlfluff/ST05",
"description": "Subqueries in Join/From clauses; use CTEs",
"tier": "P1"
"tier": "P1",
"descriptionZh": "Join/From 子句中的子查询;使用 CTE",
"descriptionJa": "Join/From 句内のサブクエリ;CTE を使用する"
},
{
"id": "sql-lint/ST06",
"id": "sqlfluff/ST06",
"description": "Column order: wildcards, simple targets, then calculations",
"tier": "P1"
"tier": "P1",
"descriptionZh": "列顺序:通配符、简单目标、然后计算",
"descriptionJa": "列の順序:ワイルドカード、単純対象、計算の順"
},
{
"id": "sql-lint/ST07",
"id": "sqlfluff/ST07",
"description": "Prefer ON over USING for join keys",
"tier": "P1"
"tier": "P1",
"descriptionZh": "连接键优先使用 ON 而非 USING",
"descriptionJa": "結合キーに USING より ON を優先する"
},
{
"id": "sql-lint/ST08",
"id": "sqlfluff/ST08",
"description": "DISTINCT used with parentheses",
"tier": "P0"
"tier": "P0",
"descriptionZh": "DISTINCT 与括号一起使用",
"descriptionJa": "DISTINCT が括弧付きで使用されている"
},
{
"id": "sql-lint/ST09",
"id": "sqlfluff/ST09",
"description": "Join condition order",
"tier": "P1"
"tier": "P1",
"descriptionZh": "连接条件顺序",
"descriptionJa": "結合条件の順序"
},
{
"id": "sql-lint/ST10",
"id": "sqlfluff/ST10",
"description": "Redundant constant expression",
"tier": "P1"
"tier": "P1",
"descriptionZh": "冗余的常量表达式",
"descriptionJa": "冗長な定数式"
},
{
"id": "sql-lint/ST11",
"id": "sqlfluff/ST11",
"description": "Joined table not referenced",
"tier": "P1"
"tier": "P1",
"descriptionZh": "被连接的表未被引用",
"descriptionJa": "結合されたテーブルが参照されていない"
},
{
"id": "sql-lint/ST12",
"id": "sqlfluff/ST12",
"description": "Consecutive semicolons",
"tier": "P1"
"tier": "P1",
"descriptionZh": "连续的分号",
"descriptionJa": "連続するセミコロン"
},
{
"id": "sql-lint/TQ01",
"id": "sqlfluff/TQ01",
"description": "SP_ prefix should not be used for user-defined stored procedures",
"tier": "P2"
"tier": "P2",
"descriptionZh": "用户定义存储过程不应使用 SP_ 前缀",
"descriptionJa": "ユーザー定義ストアドプロシージャに SP_ プレフィックスを使わない"
},
{
"id": "sql-lint/TQ02",
"id": "sqlfluff/TQ02",
"description": "Procedure bodies with multiple statements wrapped in BEGIN/END",
"tier": "P2"
"tier": "P2",
"descriptionZh": "多语句的过程体用 BEGIN/END 包裹",
"descriptionJa": "複数文のプロシージャ本体を BEGIN/END で囲む"
},
{
"id": "sql-lint/TQ03",
"id": "sqlfluff/TQ03",
"description": "Remove empty batches",
"tier": "P2"
"tier": "P2",
"descriptionZh": "移除空批次",
"descriptionJa": "空のバッチを削除する"
}
]
}
+7 -2
View File
@@ -11,6 +11,11 @@ interface RuleYamlItem {
excludeLanguages?: string[];
}
function stripQuotes(raw: string): string {
const m = raw.match(/^(['"])(.*)\1$/);
return m ? m[2] : raw;
}
function parseYamlSimple(content: string): object[] {
const items: Array<Record<string, unknown>> = [];
let current: Record<string, unknown> | null = null;
@@ -31,7 +36,7 @@ function parseYamlSimple(content: string): object[] {
s.trim().replace(/^['"]|['"]$/g, '')
);
} else {
current[key] = raw;
current[key] = stripQuotes(raw);
}
}
} else if (current) {
@@ -46,7 +51,7 @@ function parseYamlSimple(content: string): object[] {
s.trim().replace(/^['"]|['"]$/g, '')
);
} else {
current[key] = raw;
current[key] = stripQuotes(raw);
}
}
}
+251
View File
@@ -0,0 +1,251 @@
import * as vscode from 'vscode';
export interface MethodScope {
name: string;
range: vscode.Range;
code: string;
signature: string;
callers: string[];
callees: string[];
role: string;
}
export interface MethodSymbol {
name: string;
range: vscode.Range;
containerName?: string;
}
interface RawDocSymbol {
name: string;
kind: vscode.SymbolKind;
range?: vscode.Range;
children?: RawDocSymbol[];
location?: vscode.Location;
}
const CONTAINER_KINDS = new Set([
vscode.SymbolKind.Class,
vscode.SymbolKind.Interface,
vscode.SymbolKind.Namespace,
vscode.SymbolKind.Module,
vscode.SymbolKind.Object,
vscode.SymbolKind.Struct,
vscode.SymbolKind.Enum,
vscode.SymbolKind.Package,
]);
function isMethodKind(kind: vscode.SymbolKind): boolean {
return (
kind === vscode.SymbolKind.Function ||
kind === vscode.SymbolKind.Method ||
kind === vscode.SymbolKind.Constructor
);
}
function collectSymbols(symbol: RawDocSymbol, containerName: string | undefined, out: MethodSymbol[]): void {
const kind = symbol.kind;
if (isMethodKind(kind)) {
const range = symbol.range ?? symbol.location?.range;
if (range) {
out.push({ name: symbol.name, range, containerName });
}
}
const nextContainer = CONTAINER_KINDS.has(kind)
? containerName
? `${containerName}.${symbol.name}`
: symbol.name
: containerName;
for (const child of symbol.children ?? []) {
collectSymbols(child, nextContainer, out);
}
}
export async function getMethodSymbols(document: vscode.TextDocument): Promise<MethodSymbol[]> {
let raw: RawDocSymbol[] | undefined;
try {
raw = await vscode.commands.executeCommand<RawDocSymbol[]>(
'vscode.executeDocumentSymbolProvider',
document.uri
);
} catch {
raw = undefined;
}
if (raw && raw.length > 0) {
const symbols: MethodSymbol[] = [];
for (const symbol of raw) {
collectSymbols(symbol, undefined, symbols);
}
if (symbols.length > 0) {
return symbols;
}
}
return fallbackRegexSymbols(document);
}
export async function extractMethodScope(
document: vscode.TextDocument,
range: vscode.Range
): Promise<MethodScope | null> {
const symbols = await getMethodSymbols(document);
if (symbols.length === 0) { return null; }
const target = findTargetSymbol(symbols, range);
if (!target) { return null; }
const ranges = new Map<MethodSymbol, vscode.Range>();
for (const symbol of symbols) {
ranges.set(
symbol,
symbol.range.isEmpty
? expandToMethodBody(document, symbol.range.start.line)
: symbol.range
);
}
const targetRange = ranges.get(target)!;
const code = document.getText(targetRange);
if (!code.trim()) { return null; }
const callers: string[] = [];
const callees: string[] = [];
const targetPattern = new RegExp(`\\b${escapeRegExp(target.name)}\\s*\\(`);
for (const symbol of symbols) {
if (symbol === target) { continue; }
const otherCode = document.getText(ranges.get(symbol)!);
if (otherCode && targetPattern.test(otherCode)) {
callers.push(symbol.name);
}
const otherPattern = new RegExp(`\\b${escapeRegExp(symbol.name)}\\s*\\(`);
if (code && otherPattern.test(code)) {
callees.push(symbol.name);
}
}
return {
name: target.name,
range: targetRange,
code,
signature: extractSignature(code, target.name),
callers,
callees,
role: inferRole(target),
};
}
function findTargetSymbol(symbols: MethodSymbol[], range: vscode.Range): MethodSymbol | null {
const containing = symbols.filter(s => s.range.contains(range));
if (containing.length === 0) { return null; }
let best = containing[0];
for (const symbol of containing) {
if (symbol.range.start.isAfter(best.range.start)) {
best = symbol;
}
}
return best;
}
function escapeRegExp(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function extractSignature(code: string, fallbackName: string): string {
const lines = code.split('\n');
const sigLines: string[] = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) { continue; }
if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed.startsWith('*/')) {
continue;
}
sigLines.push(trimmed);
if (trimmed.includes('{') || trimmed.endsWith(')')) { break; }
}
let sig = sigLines.join(' ').replace(/\{.*$/, '').trim();
sig = sig.replace(/\s{2,}/g, ' ');
return sig || fallbackName;
}
function inferRole(symbol: MethodSymbol): string {
const container = symbol.containerName ?? '';
const name = symbol.name.toLowerCase();
if (container.includes('Controller')) { return 'HTTP 请求处理入口'; }
if (container.includes('Service')) { return '业务逻辑处理'; }
if (container.includes('Repository') || container.includes('Dao')) { return '数据访问'; }
if (name.startsWith('get') || name.startsWith('set') || name.startsWith('is')) { return '属性访问器'; }
if (name.startsWith('init') || name.startsWith('on')) { return '生命周期回调'; }
if (name.startsWith('handle') || name.startsWith('process')) { return '流程处理'; }
if (name.startsWith('build') || name.startsWith('create')) { return '工厂/构建'; }
if (name.startsWith('parse') || name.startsWith('convert') || name.startsWith('transform')) { return '数据转换'; }
return '通用方法';
}
function fallbackRegexSymbols(document: vscode.TextDocument): MethodSymbol[] {
const text = document.getText();
const lang = document.languageId;
const patterns: RegExp[] = [];
if (['typescript', 'javascript', 'typescriptreact', 'javascriptreact'].includes(lang)) {
patterns.push(/(?:async\s+)?function\s+(\w+)/g);
patterns.push(/(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?(?:function\s*)?\(/g);
} else if (['java', 'kotlin', 'go'].includes(lang)) {
patterns.push(/((?:public|private|protected|static)\s+)*\w+(?:<[^>]+>)?\s+(\w+)\s*\(/g);
}
const symbols: MethodSymbol[] = [];
const seen = new Set<string>();
for (const pattern of patterns) {
pattern.lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = pattern.exec(text)) !== null) {
const name = match[1] ?? match[2];
if (!name) { continue; }
const start = document.positionAt(match.index);
const key = `${name}@${start.line}`;
if (seen.has(key)) { continue; }
seen.add(key);
symbols.push({ name, range: new vscode.Range(start, start) });
}
}
return symbols;
}
function expandToMethodBody(document: vscode.TextDocument, startLine: number): vscode.Range {
const start = new vscode.Position(startLine, 0);
const text = document.getText();
const startOffset = document.offsetAt(start);
let depth = 0;
let inString: string | null = null;
let i = startOffset;
while (i < text.length) {
const ch = text[i];
if (inString) {
if (ch === '\\') { i += 2; continue; }
if (ch === inString) { inString = null; }
} else if (ch === '"' || ch === "'" || ch === '`') {
inString = ch;
} else if (ch === '/') {
if (text[i + 1] === '/') {
while (i < text.length && text[i] !== '\n') { i++; }
continue;
}
if (text[i + 1] === '*') {
i += 2;
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) { i++; }
i += 2;
continue;
}
} else if (ch === '{') {
depth++;
} else if (ch === '}') {
depth--;
if (depth === 0) {
return new vscode.Range(start, document.positionAt(i + 1));
}
}
i++;
}
const lastLine = document.lineCount - 1;
return new vscode.Range(start, document.lineAt(lastLine).range.end);
}
+36
View File
@@ -0,0 +1,36 @@
import * as vscode from 'vscode';
export interface ReviewStatus {
issueCount: number;
timestamp: number;
}
export class ReviewStatusCache {
private cache = new Map<string, ReviewStatus>();
get(uri: vscode.Uri, methodName: string): ReviewStatus | null {
const key = this.buildKey(uri, methodName);
return this.cache.get(key) ?? null;
}
set(uri: vscode.Uri, methodName: string, issueCount: number): void {
const key = this.buildKey(uri, methodName);
this.cache.set(key, {
issueCount,
timestamp: Date.now(),
});
}
clearDocument(uri: vscode.Uri): void {
const prefix = uri.toString() + '::';
for (const key of this.cache.keys()) {
if (key.startsWith(prefix)) {
this.cache.delete(key);
}
}
}
private buildKey(uri: vscode.Uri, methodName: string): string {
return uri.toString() + '::' + methodName;
}
}
+56
View File
@@ -0,0 +1,56 @@
import * as vscode from 'vscode';
import { getMethodSymbols } from '../scope/method-extractor';
import { ReviewStatusCache, type ReviewStatus } from '../scope/status-cache';
import { t } from '../i18n/messages';
export class MethodCodeLensProvider implements vscode.CodeLensProvider {
private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>();
readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
constructor(private statusCache: ReviewStatusCache) {}
refresh(): void {
this._onDidChangeCodeLenses.fire();
}
async provideCodeLenses(
document: vscode.TextDocument,
token: vscode.CancellationToken
): Promise<vscode.CodeLens[]> {
const config = vscode.workspace.getConfiguration('vscode-code-reviewer');
const enabled = config.get<boolean>('codelens.enabled', true);
if (!enabled) { return []; }
const languages = config.get<string[]>('codelens.languages', [
'typescript', 'javascript', 'java', 'python'
]);
if (!languages.includes(document.languageId)) { return []; }
const symbols = await getMethodSymbols(document);
if (symbols.length === 0) { return []; }
if (symbols.length > 50) { return []; }
const lenses: vscode.CodeLens[] = [];
for (const symbol of symbols) {
const status = this.statusCache.get(document.uri, symbol.name);
const title = this.buildLensTitle(status);
const line = symbol.range.start.line;
lenses.push(new vscode.CodeLens(new vscode.Range(line, 0, line, 0), {
command: 'codeReviewer.reviewMethod',
title,
arguments: [symbol.range],
}));
}
return lenses;
}
private buildLensTitle(status: ReviewStatus | null): string {
if (!status) {
return t('codelens.reviewMethod');
}
if (status.issueCount === 0) {
return t('codelens.reviewedClean');
}
return t('codelens.reviewedWithIssues', { 0: String(status.issueCount) });
}
}
+26 -23
View File
@@ -16,7 +16,7 @@ import { ExcelConverter } from '../rules/converters/excel-converter';
import { DocxConverter } from '../rules/converters/docx-converter';
import { PptxConverter } from '../rules/converters/pptx-converter';
import { t, onLanguageChange } from '../i18n/messages';
import { getEslintConfigPath, getStylelintConfigPath, getPMDRulesetPath, getSqlLintConfigFile, isAdapterEnabled, setAdapterEnabled } from '../config/linter';
import { getEslintConfigPath, getStylelintConfigPath, getPMDRulesetPath, getSqlFluffConfigFile, isAdapterEnabled, setAdapterEnabled } from '../config/linter';
type ConfigMode = 'builtin' | 'project' | 'global';
@@ -52,22 +52,22 @@ function getPmdRulesetTemplate(): string {
function getSqlfluffTemplate(): string {
return `[sqlfluff]
# ${t('setup.template.sqlfluffDialect')}
dialect = postgres
dialect = mysql
# ${t('setup.template.sqlfluffRules')}
rules = all`;
}
function getEslintTemplate(): string {
return `module.exports = {
root: true,
env: { node: true, es2022: true },
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
rules: {
'no-unused-vars': 'warn', // ${t('setup.template.eslintComment1')}
'no-console': 'off', // ${t('setup.template.eslintComment2')}
'semi': ['error', 'always'], // ${t('setup.template.eslintComment3')}
return `module.exports = [
{
languageOptions: { ecmaVersion: 2022, sourceType: 'module' },
rules: {
'no-unused-vars': 'warn', // ${t('setup.template.eslintComment1')}
'no-console': 'off', // ${t('setup.template.eslintComment2')}
'semi': ['error', 'always'], // ${t('setup.template.eslintComment3')}
},
},
};`;
];`;
}
function getStylelintTemplate(): string {
@@ -98,10 +98,10 @@ const ADAPTER_METADATA: Record<string, {
configFileTemplate: getPmdRulesetTemplate,
i18nKey: 'pmd',
},
'sql-lint': {
name: 'SQL-Lint',
'sqlfluff': {
name: 'SQLFluff',
projectConfigFileName: '.sqlfluff',
settingsTarget: 'vscode-code-reviewer.sql-lint',
settingsTarget: 'vscode-code-reviewer.sqlfluff',
hasExternalDependency: true,
dependencyLabel: 'Python + sqlfluff',
configFileTemplate: getSqlfluffTemplate,
@@ -109,7 +109,7 @@ const ADAPTER_METADATA: Record<string, {
},
eslint: {
name: 'ESLint',
projectConfigFileName: '.eslintrc.js',
projectConfigFileName: 'eslint.config.js',
settingsTarget: 'vscode-code-reviewer.linters',
hasExternalDependency: false,
configFileTemplate: getEslintTemplate,
@@ -126,17 +126,17 @@ const ADAPTER_METADATA: Record<string, {
};
const PROJECT_CONFIG_FILES: Record<string, string[]> = {
eslint: ['.eslintrc.js', '.eslintrc.json', '.eslintrc.yaml', '.eslintrc.yml', '.eslintrc', 'eslint.config.js', 'eslint.config.mjs'],
eslint: ['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs', 'eslint.config.ts', 'eslint.config.mts', 'eslint.config.cts'],
stylelint: ['.stylelintrc.js', '.stylelintrc.json', '.stylelintrc.yaml', '.stylelintrc.yml', '.stylelintrc', 'stylelint.config.js'],
pmd: ['ruleset.xml'],
'sql-lint': ['.sqlfluff'],
'sqlfluff': ['.sqlfluff'],
};
const GLOBAL_CONFIG_GETTERS: Record<string, () => string> = {
eslint: getEslintConfigPath,
stylelint: getStylelintConfigPath,
pmd: getPMDRulesetPath,
'sql-lint': getSqlLintConfigFile,
'sqlfluff': getSqlFluffConfigFile,
};
export class SetupViewProvider implements vscode.WebviewViewProvider {
@@ -305,7 +305,7 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
e.affectsConfiguration('vscode-code-reviewer.linter') ||
e.affectsConfiguration('vscode-code-reviewer.linters') ||
e.affectsConfiguration('vscode-code-reviewer.pmd') ||
e.affectsConfiguration('vscode-code-reviewer.sql-lint')
e.affectsConfiguration('vscode-code-reviewer.sqlfluff')
) {
this.pushConfig();
}
@@ -397,12 +397,15 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
try {
const provider = createProvider(config.provider, apiKey, config.baseUrl, this.context.extensionUri);
await provider.chat('回复 ok', 'ping', {
const result = await provider.chat('回复 ok', 'ping', {
model: config.model,
temperature: 0,
maxTokens: 1024,
timeoutMs: 15000,
});
if (!result || result.trim() === '') {
throw new Error(t('setup.emptyResponse'));
}
this.connectionTested = true;
this.connectionSuccess = true;
await this.saveConnectionState();
@@ -452,7 +455,7 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
);
});
const decision = await showImportPreview(conversion);
const decision = await showImportPreview(conversion, this.context);
if (!decision || !decision.confirmed) {
vscode.window.showInformationMessage(t('setup.importCancelled'));
return;
@@ -512,7 +515,7 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
title: t('setup.importing'),
}, () => this.importService.convert(srcPath, this.context));
const decision = await showImportPreview(conversion);
const decision = await showImportPreview(conversion, this.context);
if (!decision || !decision.confirmed) {
vscode.window.showInformationMessage(t('setup.importCancelled'));
return;
@@ -1158,7 +1161,7 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco
if (meta.hasExternalDependency) {
if (id === 'pmd') {
dependencyStatus = this.checkJavaReady() ? 'ready' : 'missing';
} else if (id === 'sql-lint') {
} else if (id === 'sqlfluff') {
dependencyStatus = this.checkPythonReady() ? 'ready' : 'missing';
}
}