kcode-pi 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -2
- package/dist/cli/kcode.d.ts +1 -0
- package/dist/cli/kcode.js +27 -4
- package/package.json +1 -1
- package/src/cli/kcode.ts +29 -4
- package/src/official/kingdee-skills.ts +60 -13
- package/src/rules/checker.ts +143 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/SKILL.md +2 -2
- package/vendor/kingdee-skills/ok-cosmic/SKILL.md +52 -101
- package/vendor/kingdee-skills/ok-cosmic/agents/openai.yaml +4 -4
- package/vendor/kingdee-skills/ok-cosmic/manifest.json +21 -20
- package/vendor/kingdee-skills/ok-cosmic/ok-cosmic-intro.html +1 -1
- package/vendor/kingdee-skills/ok-cosmic/rules/a-layer-rules.json +1 -1
- package/vendor/kingdee-skills/ok-cosmic/rules/anti-patterns.md +2 -2
- package/vendor/kingdee-skills/ok-cosmic/rules/coding-preferences.md +4 -4
- package/vendor/kingdee-skills/ok-cosmic/rules/constraints.md +3 -3
- package/vendor/kingdee-skills/ok-cosmic/rules/decision-matrix.md +8 -8
- package/vendor/kingdee-skills/ok-cosmic/rules/intent-routing.md +1 -1
- package/vendor/kingdee-skills/ok-cosmic/rules/post-check.md +19 -18
- package/vendor/kingdee-skills/ok-ksql/SKILL.md +9 -9
- package/vendor/kingdee-skills/ok-ksql/manifest.json +2 -1
- package/vendor/kingdee-skills/ok-ksql/references/ksql-datafix.md +2 -2
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/pattern-matcher.py +0 -336
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/review-score-calculator.py +0 -121
- package/vendor/kingdee-skills/ok-cosmic/CHANGELOG.md +0 -295
- package/vendor/kingdee-skills/ok-cosmic/README.md +0 -460
- package/vendor/kingdee-skills/ok-cosmic/requirements.txt +0 -2
- package/vendor/kingdee-skills/ok-cosmic/scripts/config_loader.py +0 -204
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-api-knowledge.py +0 -910
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-basedata-query.py +0 -359
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-config-check.py +0 -181
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-extpoints-query.py +0 -389
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-form-metadata.py +0 -856
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-check.py +0 -262
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-lint.py +0 -293
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/__init__.py +0 -2
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/base.py +0 -393
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/resource_check.py +0 -176
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/scene_check.py +0 -375
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/style_check.py +0 -434
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/verify_check.py +0 -36
- package/vendor/kingdee-skills/ok-cosmic/scripts/route_client.py +0 -186
- package/vendor/kingdee-skills/ok-cosmic/scripts/script_utils.py +0 -40
- package/vendor/kingdee-skills/ok-cosmic/scripts/sqlite_cache.py +0 -142
- package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-commons.jar +0 -0
- package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-features.jar +0 -0
- package/vendor/kingdee-skills/ok-cosmic/setup/setup-mac.sh +0 -18
- package/vendor/kingdee-skills/ok-cosmic/setup/setup-windows.bat +0 -53
- package/vendor/kingdee-skills/ok-cosmic/setup/setup.jar +0 -0
- package/vendor/kingdee-skills/ok-ksql/scripts/ksql_lint.py +0 -363
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""公共数据结构与工具函数,所有检查模块共享。"""
|
|
3
|
-
|
|
4
|
-
import re
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
|
-
from enum import Enum
|
|
7
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
8
|
-
|
|
9
|
-
import tree_sitter
|
|
10
|
-
import tree_sitter_java
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# ──────────────────────────────────────────────
|
|
14
|
-
# 数据结构
|
|
15
|
-
# ──────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
class Severity(str, Enum):
|
|
18
|
-
ERROR = "ERROR" # 必定错误
|
|
19
|
-
WARNING = "WARNING" # 场景错配 / 强烈不推荐
|
|
20
|
-
INFO = "INFO" # 风格偏好 / 建议
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@dataclass
|
|
24
|
-
class LintIssue:
|
|
25
|
-
file: str
|
|
26
|
-
line: int
|
|
27
|
-
severity: Severity
|
|
28
|
-
rule_id: str
|
|
29
|
-
message: str
|
|
30
|
-
fix_hint: str = ""
|
|
31
|
-
source_line: str = ""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@dataclass
|
|
35
|
-
class LintReport:
|
|
36
|
-
total_files: int = 0
|
|
37
|
-
total_issues: int = 0
|
|
38
|
-
errors: int = 0
|
|
39
|
-
warnings: int = 0
|
|
40
|
-
infos: int = 0
|
|
41
|
-
issues: List[LintIssue] = field(default_factory=list)
|
|
42
|
-
|
|
43
|
-
def add(self, issue: LintIssue):
|
|
44
|
-
self.issues.append(issue)
|
|
45
|
-
self.total_issues += 1
|
|
46
|
-
if issue.severity == Severity.ERROR:
|
|
47
|
-
self.errors += 1
|
|
48
|
-
elif issue.severity == Severity.WARNING:
|
|
49
|
-
self.warnings += 1
|
|
50
|
-
else:
|
|
51
|
-
self.infos += 1
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# ──────────────────────────────────────────────
|
|
55
|
-
# 注释 / 字符串状态机
|
|
56
|
-
# ──────────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
def classify_lines(lines: List[str]) -> List[bool]:
|
|
59
|
-
"""对每行标记是否为"纯注释或空行"(True=非代码行,应跳过 lint)。
|
|
60
|
-
|
|
61
|
-
使用状态机追踪多行注释块(/* ... */),
|
|
62
|
-
单行内同时识别行注释、字符串字面量,避免误判。
|
|
63
|
-
支持行中间开始的块注释(如 `code; /* comment start`)。
|
|
64
|
-
"""
|
|
65
|
-
result: List[bool] = []
|
|
66
|
-
in_block_comment = False
|
|
67
|
-
in_text_block = False
|
|
68
|
-
|
|
69
|
-
for line in lines:
|
|
70
|
-
if in_text_block:
|
|
71
|
-
end_idx = line.find('"""')
|
|
72
|
-
if end_idx >= 0:
|
|
73
|
-
in_text_block = False
|
|
74
|
-
remainder = line[end_idx + 3:].strip()
|
|
75
|
-
result.append(not bool(remainder))
|
|
76
|
-
else:
|
|
77
|
-
result.append(True)
|
|
78
|
-
continue
|
|
79
|
-
|
|
80
|
-
if in_block_comment:
|
|
81
|
-
# 当前处于 /* ... */ 块注释中
|
|
82
|
-
end_idx = line.find("*/")
|
|
83
|
-
if end_idx >= 0:
|
|
84
|
-
in_block_comment = False
|
|
85
|
-
# 关闭后如果剩余部分有实质代码,视为代码行
|
|
86
|
-
remainder = line[end_idx + 2:].strip()
|
|
87
|
-
result.append(not bool(remainder))
|
|
88
|
-
else:
|
|
89
|
-
result.append(True)
|
|
90
|
-
continue
|
|
91
|
-
|
|
92
|
-
stripped = line.strip()
|
|
93
|
-
|
|
94
|
-
# 检查是否进入 Text Block (""")
|
|
95
|
-
if '"""' in stripped and stripped.count('"""') % 2 != 0:
|
|
96
|
-
in_text_block = True
|
|
97
|
-
result.append(False) # 开启"""的这一行本身属于代码声明
|
|
98
|
-
continue
|
|
99
|
-
stripped = line.strip()
|
|
100
|
-
# 空行
|
|
101
|
-
if not stripped:
|
|
102
|
-
result.append(True)
|
|
103
|
-
continue
|
|
104
|
-
# 纯单行注释
|
|
105
|
-
if stripped.startswith("//"):
|
|
106
|
-
result.append(True)
|
|
107
|
-
continue
|
|
108
|
-
# Javadoc / 多行注释续行(行首 *)
|
|
109
|
-
if stripped.startswith("*") and not stripped.startswith("*/"):
|
|
110
|
-
result.append(True)
|
|
111
|
-
continue
|
|
112
|
-
# 检查块注释开始(行首或行中间)
|
|
113
|
-
if stripped.startswith("/*"):
|
|
114
|
-
close_idx = stripped.find("*/", 2)
|
|
115
|
-
if close_idx >= 0:
|
|
116
|
-
# 单行块注释 /* ... */,检查 */ 后是否有代码
|
|
117
|
-
remainder = stripped[close_idx + 2:].strip()
|
|
118
|
-
result.append(not bool(remainder))
|
|
119
|
-
else:
|
|
120
|
-
in_block_comment = True
|
|
121
|
-
result.append(True)
|
|
122
|
-
continue
|
|
123
|
-
|
|
124
|
-
# 代码行:检查行中间是否有未闭合的 /* 开始块注释
|
|
125
|
-
# 需要排除字符串字面量内的 /*
|
|
126
|
-
if _has_unclosed_block_comment_start(stripped):
|
|
127
|
-
in_block_comment = True
|
|
128
|
-
# 行本身有代码部分,标记为代码行
|
|
129
|
-
result.append(False)
|
|
130
|
-
continue
|
|
131
|
-
|
|
132
|
-
# 其他情况:代码行(可能包含行尾注释,但行本身有代码)
|
|
133
|
-
result.append(False)
|
|
134
|
-
|
|
135
|
-
return result
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def _has_unclosed_block_comment_start(line: str) -> bool:
|
|
139
|
-
"""检测行中间是否有未闭合的 /* 块注释开始(排除字符串内的 /*)。"""
|
|
140
|
-
i = 0
|
|
141
|
-
in_str = False
|
|
142
|
-
str_char = ''
|
|
143
|
-
while i < len(line):
|
|
144
|
-
c = line[i]
|
|
145
|
-
if in_str:
|
|
146
|
-
if c == '\\' and i + 1 < len(line):
|
|
147
|
-
i += 2 # 跳过转义字符
|
|
148
|
-
continue
|
|
149
|
-
if c == str_char:
|
|
150
|
-
in_str = False
|
|
151
|
-
i += 1
|
|
152
|
-
continue
|
|
153
|
-
if c in ('"', "'"):
|
|
154
|
-
in_str = True
|
|
155
|
-
str_char = c
|
|
156
|
-
i += 1
|
|
157
|
-
continue
|
|
158
|
-
if c == '/' and i + 1 < len(line):
|
|
159
|
-
if line[i + 1] == '/':
|
|
160
|
-
return False # 行注释,不可能有 /* 了
|
|
161
|
-
if line[i + 1] == '*':
|
|
162
|
-
# 找到 /*,检查本行内是否有 */
|
|
163
|
-
close_idx = line.find('*/', i + 2)
|
|
164
|
-
if close_idx >= 0:
|
|
165
|
-
# 同行闭合 /* ... */,跳过继续
|
|
166
|
-
i = close_idx + 2
|
|
167
|
-
continue
|
|
168
|
-
else:
|
|
169
|
-
return True # 未闭合的 /* 块注释开始
|
|
170
|
-
i += 1
|
|
171
|
-
return False
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def is_comment_or_string(line: str) -> bool:
|
|
175
|
-
"""粗略判断当前行是否为注释或字符串(排除误报)。
|
|
176
|
-
|
|
177
|
-
注意:此函数为单行判断,无法追踪跨行 /* */ 状态。
|
|
178
|
-
对跨行精确判断,请使用 classify_lines() 代替。
|
|
179
|
-
"""
|
|
180
|
-
stripped = line.strip()
|
|
181
|
-
if not stripped:
|
|
182
|
-
return True
|
|
183
|
-
return (stripped.startswith("//")
|
|
184
|
-
or stripped.startswith("*")
|
|
185
|
-
or stripped.startswith("/*"))
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def strip_line_comment(line: str) -> str:
|
|
189
|
-
"""移除单行注释部分,保留代码主体(正确跳过字符串字面量内的 //)。"""
|
|
190
|
-
i = 0
|
|
191
|
-
in_str = False
|
|
192
|
-
str_char = ''
|
|
193
|
-
while i < len(line):
|
|
194
|
-
c = line[i]
|
|
195
|
-
if in_str:
|
|
196
|
-
if c == '\\' and i + 1 < len(line):
|
|
197
|
-
i += 2
|
|
198
|
-
continue
|
|
199
|
-
if c == str_char:
|
|
200
|
-
in_str = False
|
|
201
|
-
i += 1
|
|
202
|
-
continue
|
|
203
|
-
if c in ('"', "'"):
|
|
204
|
-
in_str = True
|
|
205
|
-
str_char = c
|
|
206
|
-
i += 1
|
|
207
|
-
continue
|
|
208
|
-
if c == '/' and i + 1 < len(line) and line[i + 1] == '/':
|
|
209
|
-
return line[:i]
|
|
210
|
-
i += 1
|
|
211
|
-
return line
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def strip_string_literals(line: str) -> str:
|
|
215
|
-
"""移除字符串字面量,便于做结构分析。"""
|
|
216
|
-
return re.sub(r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'', '""', line)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def code_for_structure(line: str) -> str:
|
|
220
|
-
"""用于结构分析的代码行:去掉行注释与字符串。"""
|
|
221
|
-
return strip_line_comment(strip_string_literals(line))
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
METHOD_DECL_RE = re.compile(
|
|
225
|
-
r"^\s*(?:@\w+(?:\([^)]*\))?\s*)*"
|
|
226
|
-
r"(?:public|protected|private)\s+"
|
|
227
|
-
r"(?:static\s+|final\s+|synchronized\s+|abstract\s+|native\s+|default\s+)*"
|
|
228
|
-
r"[\w<>\[\], ?.@]+\s+([A-Za-z_]\w*)\s*"
|
|
229
|
-
r"\([^;{}]*\)\s*(?:throws\s+[^{]+)?\s*(?:\{)?\s*$"
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
LOOP_HEADER_RE = re.compile(r"\bfor\s*\(|\bwhile\s*\(|^\s*do\b")
|
|
233
|
-
LAMBDA_ITER_RE = re.compile(r"\.\s*(forEach|map|flatMap|peek)\s*\(")
|
|
234
|
-
CLASS_DECL_RE = re.compile(r"\bclass\s+[A-Za-z_]\w*")
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
def looks_like_loop_header(line: str) -> bool:
|
|
238
|
-
"""判断当前行是否看起来是 loop 头。"""
|
|
239
|
-
return bool(LOOP_HEADER_RE.search(code_for_structure(line)))
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def parse_java(lines: List[str]) -> Tuple[Any, Any]:
|
|
243
|
-
"""解析 Java 源码,返回 (tree, language) 供多处复用,避免重复解析。"""
|
|
244
|
-
lang = tree_sitter.Language(tree_sitter_java.language())
|
|
245
|
-
parser = tree_sitter.Parser(lang)
|
|
246
|
-
src_bytes = "\n".join(lines).encode('utf-8')
|
|
247
|
-
tree = parser.parse(src_bytes)
|
|
248
|
-
return tree, lang
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def analyze_java_context(lines: List[str], tree=None) -> Tuple[List[Optional[str]], List[bool]]:
|
|
252
|
-
"""
|
|
253
|
-
基于 AST 构建上下文:
|
|
254
|
-
- 每行所在方法名
|
|
255
|
-
- 每行是否位于循环体中
|
|
256
|
-
"""
|
|
257
|
-
if tree is None:
|
|
258
|
-
tree, _ = parse_java(lines)
|
|
259
|
-
|
|
260
|
-
method_ctx: List[Optional[str]] = [None] * len(lines)
|
|
261
|
-
loop_ctx: List[bool] = [False] * len(lines)
|
|
262
|
-
|
|
263
|
-
def walk(node, current_method=None, in_loop=False):
|
|
264
|
-
m_name = current_method
|
|
265
|
-
if node.type in ("method_declaration", "constructor_declaration"):
|
|
266
|
-
name_node = node.child_by_field_name("name")
|
|
267
|
-
if name_node:
|
|
268
|
-
m_name = name_node.text.decode('utf-8')
|
|
269
|
-
|
|
270
|
-
is_loop = in_loop
|
|
271
|
-
if node.type in ("for_statement", "enhanced_for_statement", "while_statement", "do_statement"):
|
|
272
|
-
is_loop = True
|
|
273
|
-
elif node.type == "method_invocation":
|
|
274
|
-
name_node = node.child_by_field_name("name")
|
|
275
|
-
if name_node and name_node.text.decode('utf-8') in ("forEach", "map", "flatMap", "peek"):
|
|
276
|
-
is_loop = True
|
|
277
|
-
|
|
278
|
-
start_row = node.start_point[0]
|
|
279
|
-
end_row = node.end_point[0]
|
|
280
|
-
for r in range(start_row, end_row + 1):
|
|
281
|
-
if r < len(lines):
|
|
282
|
-
method_ctx[r] = m_name
|
|
283
|
-
if is_loop:
|
|
284
|
-
loop_ctx[r] = True
|
|
285
|
-
|
|
286
|
-
for child in node.children:
|
|
287
|
-
walk(child, m_name, is_loop)
|
|
288
|
-
|
|
289
|
-
walk(tree.root_node)
|
|
290
|
-
return method_ctx, loop_ctx
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
# 操作插件的类标识模式
|
|
294
|
-
OP_PLUGIN_MARKERS = [
|
|
295
|
-
r"extends\s+AbstractOperationServicePlugIn\b",
|
|
296
|
-
r"extends\s+AbstractOperationServicePlugInExt\b",
|
|
297
|
-
r"extends\s+AbstractValidatorExt\b",
|
|
298
|
-
r"implements\s+.*OperationServicePlugIn\b",
|
|
299
|
-
]
|
|
300
|
-
|
|
301
|
-
# UI 插件标识模式
|
|
302
|
-
UI_PLUGIN_MARKERS = [
|
|
303
|
-
r"extends\s+AbstractFormPluginExt\b",
|
|
304
|
-
r"extends\s+AbstractBillPlugInExt\b",
|
|
305
|
-
r"extends\s+AbstractListPluginExt\b",
|
|
306
|
-
r"extends\s+AbstractFormPlugin\b",
|
|
307
|
-
r"extends\s+AbstractBillPlugIn\b",
|
|
308
|
-
r"extends\s+AbstractListPlugin\b",
|
|
309
|
-
r"extends\s+AbstractTreeListPlugin\b",
|
|
310
|
-
r"extends\s+StandardTreeListPlugin\b",
|
|
311
|
-
]
|
|
312
|
-
|
|
313
|
-
# 其他继承型插件标识模式(非操作、非 UI,但仍需检查 super 调用)
|
|
314
|
-
OTHER_INHERITABLE_MARKERS = [
|
|
315
|
-
r"extends\s+AbstractConvertPlugIn\b",
|
|
316
|
-
r"extends\s+AbstractWriteBackPlugIn\b",
|
|
317
|
-
r"extends\s+AbstractReportFormPlugin\b",
|
|
318
|
-
r"extends\s+AbstractReportListDataPlugin\b",
|
|
319
|
-
r"extends\s+AbstractPrintPlugin\b",
|
|
320
|
-
r"extends\s+AbstractTask\b",
|
|
321
|
-
r"extends\s+BatchImportPlugin\b",
|
|
322
|
-
]
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def detect_plugin_type(lines: List[str]) -> Tuple[bool, bool, bool]:
|
|
326
|
-
"""检测文件是操作插件、UI 插件还是其他继承型插件,返回 (is_op, is_ui, is_other_inheritable)"""
|
|
327
|
-
full_text = "\n".join(lines)
|
|
328
|
-
is_op = any(re.search(p, full_text) for p in OP_PLUGIN_MARKERS)
|
|
329
|
-
is_ui = any(re.search(p, full_text) for p in UI_PLUGIN_MARKERS)
|
|
330
|
-
is_other = any(re.search(p, full_text) for p in OTHER_INHERITABLE_MARKERS)
|
|
331
|
-
return is_op, is_ui, is_other
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
def _detect_plugin_kind(text: str) -> Optional[str]:
|
|
335
|
-
"""基于类声明文本判断当前类属于哪种插件类型。"""
|
|
336
|
-
if any(re.search(p, text) for p in OP_PLUGIN_MARKERS):
|
|
337
|
-
return "op"
|
|
338
|
-
if any(re.search(p, text) for p in UI_PLUGIN_MARKERS):
|
|
339
|
-
return "ui"
|
|
340
|
-
if any(re.search(p, text) for p in OTHER_INHERITABLE_MARKERS):
|
|
341
|
-
return "other"
|
|
342
|
-
return None
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
def analyze_plugin_context(lines: List[str]) -> List[Optional[str]]:
|
|
346
|
-
"""
|
|
347
|
-
基于类声明与 brace 深度分析每一行所在的插件上下文:
|
|
348
|
-
- op: 操作插件类
|
|
349
|
-
- ui: UI 插件类
|
|
350
|
-
- other: 其他继承型插件类
|
|
351
|
-
- None: 非插件类 / 无插件上下文
|
|
352
|
-
"""
|
|
353
|
-
contexts: List[Optional[str]] = [None] * len(lines)
|
|
354
|
-
brace_depth = 0
|
|
355
|
-
class_stack: List[Tuple[int, Optional[str]]] = []
|
|
356
|
-
pending_class_decl: List[str] = []
|
|
357
|
-
|
|
358
|
-
for i, line in enumerate(lines):
|
|
359
|
-
code = code_for_structure(line)
|
|
360
|
-
stripped = code.strip()
|
|
361
|
-
contexts[i] = class_stack[-1][1] if class_stack else None
|
|
362
|
-
|
|
363
|
-
if pending_class_decl:
|
|
364
|
-
if stripped:
|
|
365
|
-
pending_class_decl.append(stripped)
|
|
366
|
-
if "{" in stripped:
|
|
367
|
-
class_text = " ".join(pending_class_decl)
|
|
368
|
-
class_stack.append((brace_depth + 1, _detect_plugin_kind(class_text)))
|
|
369
|
-
pending_class_decl = []
|
|
370
|
-
elif CLASS_DECL_RE.search(stripped):
|
|
371
|
-
pending_class_decl = [stripped]
|
|
372
|
-
if "{" in stripped:
|
|
373
|
-
class_text = " ".join(pending_class_decl)
|
|
374
|
-
class_stack.append((brace_depth + 1, _detect_plugin_kind(class_text)))
|
|
375
|
-
pending_class_decl = []
|
|
376
|
-
|
|
377
|
-
brace_depth += code.count("{") - code.count("}")
|
|
378
|
-
|
|
379
|
-
while class_stack and brace_depth < class_stack[-1][0]:
|
|
380
|
-
class_stack.pop()
|
|
381
|
-
|
|
382
|
-
return contexts
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
def detect_listener_interfaces(lines: List[str]) -> List[str]:
|
|
386
|
-
"""检测文件实现了哪些 Listener 接口"""
|
|
387
|
-
listeners = []
|
|
388
|
-
for line in lines:
|
|
389
|
-
m = re.search(r"implements\s+(.+?)(?:\s*\{|$)", line)
|
|
390
|
-
if m:
|
|
391
|
-
ifaces = [s.strip() for s in m.group(1).split(",")]
|
|
392
|
-
listeners.extend(i for i in ifaces if "Listener" in i)
|
|
393
|
-
return listeners
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""资源管理检查 (RESOURCE-*) — 来源: coding-preferences.md"""
|
|
3
|
-
|
|
4
|
-
import re
|
|
5
|
-
from typing import List, Dict, Optional, Set, Tuple
|
|
6
|
-
|
|
7
|
-
from .base import LintIssue, Severity, analyze_java_context, code_for_structure
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# 资源持有规则列表
|
|
11
|
-
RESOURCE_RULES = [
|
|
12
|
-
{
|
|
13
|
-
"pattern": r"(private|protected|public)\s+(DataSet|InputStream|OutputStream|Connection|ResultSet)\s+\w+\s*;",
|
|
14
|
-
"rule_id": "RESOURCE-001",
|
|
15
|
-
"severity": Severity.WARNING,
|
|
16
|
-
"message": "插件成员变量不应持有 DataSet/InputStream 等资源对象,有序列化问题",
|
|
17
|
-
"fix_hint": "在方法内使用后立即关闭,不持有引用",
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"pattern": r"static\s+(?!final\b)\w+\s+\w+\s*=",
|
|
21
|
-
"rule_id": "RESOURCE-002",
|
|
22
|
-
"severity": Severity.WARNING,
|
|
23
|
-
"message": "不应使用非 final 的 static 变量存储状态,多实例会冲突",
|
|
24
|
-
"fix_hint": "使用 PageCache 或实例变量",
|
|
25
|
-
"exclude_pattern": r"(static\s+final|static\s+\w+\s+[A-Z_]+\s*=|LogFactory|getLogger|Log\s+log)",
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
"pattern": r"static\s+final\s+String\s+\w+\s*=\s*ResManager\s*\.\s*loadKDString\s*\(",
|
|
29
|
-
"rule_id": "RESOURCE-003",
|
|
30
|
-
"severity": Severity.WARNING,
|
|
31
|
-
"message": "不要将 ResManager.loadKDString(...) 固化为 static final 常量,运行时语言切换会失效",
|
|
32
|
-
"fix_hint": "改为方法内实时调用 ResManager.loadKDString(...)",
|
|
33
|
-
},
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
DATASET_VAR_DECL_PATTERN = re.compile(r"\bDataSet\s+([A-Za-z_]\w*)\s*=")
|
|
37
|
-
DATASET_CLOSE_PATTERN = re.compile(r"\b([A-Za-z_]\w*)\s*\.\s*close\s*\(")
|
|
38
|
-
DATASET_TRY_WITH_RESOURCE_PATTERN = re.compile(r"\btry\s*\(\s*DataSet\s+([A-Za-z_]\w*)\s*=")
|
|
39
|
-
DATASET_RETURN_PATTERN = re.compile(r"\breturn\s+([A-Za-z_]\w*)\s*;")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _collect_method_ranges(
|
|
43
|
-
method_context: List[Optional[str]], total: int
|
|
44
|
-
) -> List[Tuple[Optional[str], int, int]]:
|
|
45
|
-
"""从 method_context 收集连续同方法行范围 [(method_name, start, end), ...]。"""
|
|
46
|
-
ranges: List[Tuple[Optional[str], int, int]] = []
|
|
47
|
-
if not method_context:
|
|
48
|
-
return ranges
|
|
49
|
-
cur_method = method_context[0]
|
|
50
|
-
start = 0
|
|
51
|
-
for i in range(1, total):
|
|
52
|
-
if method_context[i] != cur_method:
|
|
53
|
-
ranges.append((cur_method, start, i))
|
|
54
|
-
cur_method = method_context[i]
|
|
55
|
-
start = i
|
|
56
|
-
ranges.append((cur_method, start, total))
|
|
57
|
-
return ranges
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def check(filepath: str, lines: List[str]) -> List[LintIssue]:
|
|
61
|
-
"""执行资源管理检查,返回问题列表。"""
|
|
62
|
-
issues: List[LintIssue] = []
|
|
63
|
-
method_context, _ = analyze_java_context(lines)
|
|
64
|
-
|
|
65
|
-
# ── 逐行:RESOURCE-001 / 002 / 003(文件级即可) ──
|
|
66
|
-
for i, line in enumerate(lines):
|
|
67
|
-
lineno = i + 1
|
|
68
|
-
code_line = code_for_structure(line)
|
|
69
|
-
for rule in RESOURCE_RULES:
|
|
70
|
-
exclude = rule.get("exclude_pattern")
|
|
71
|
-
if exclude and re.search(exclude, code_line):
|
|
72
|
-
continue
|
|
73
|
-
if re.search(rule["pattern"], code_line):
|
|
74
|
-
issues.append(LintIssue(
|
|
75
|
-
file=filepath, line=lineno,
|
|
76
|
-
severity=rule["severity"],
|
|
77
|
-
rule_id=rule["rule_id"],
|
|
78
|
-
message=rule["message"],
|
|
79
|
-
fix_hint=rule["fix_hint"],
|
|
80
|
-
source_line=line.strip(),
|
|
81
|
-
))
|
|
82
|
-
|
|
83
|
-
# ── RESOURCE-004:按方法级隔离追踪 DataSet 生命周期 ──
|
|
84
|
-
method_ranges = _collect_method_ranges(method_context, len(lines))
|
|
85
|
-
for method_name, m_start, m_end in method_ranges:
|
|
86
|
-
if method_name is None:
|
|
87
|
-
continue
|
|
88
|
-
_check_dataset_lifecycle(filepath, lines, m_start, m_end, issues)
|
|
89
|
-
|
|
90
|
-
return issues
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _check_dataset_lifecycle(
|
|
94
|
-
filepath: str, lines: List[str], m_start: int, m_end: int,
|
|
95
|
-
issues: List[LintIssue],
|
|
96
|
-
) -> None:
|
|
97
|
-
"""在一个方法范围内追踪 DataSet 声明/关闭/消费状态。"""
|
|
98
|
-
dataset_vars: Dict[str, int] = {} # name -> declaration line
|
|
99
|
-
closed_vars: Set[str] = set()
|
|
100
|
-
consumed_vars: Set[str] = set()
|
|
101
|
-
in_try_resource = False
|
|
102
|
-
try_paren_depth = 0
|
|
103
|
-
# 用于跨行消费检测:记录最近的 DataSet 声明变量名
|
|
104
|
-
pending_dataset_decl: Optional[str] = None
|
|
105
|
-
|
|
106
|
-
for i in range(m_start, m_end):
|
|
107
|
-
lineno = i + 1
|
|
108
|
-
code_line = code_for_structure(lines[i])
|
|
109
|
-
|
|
110
|
-
# ── 声明检测 ──
|
|
111
|
-
decl_match = DATASET_VAR_DECL_PATTERN.search(code_line)
|
|
112
|
-
if decl_match:
|
|
113
|
-
var_name = decl_match.group(1)
|
|
114
|
-
# 同方法内同名变量取最新声明行
|
|
115
|
-
dataset_vars[var_name] = lineno
|
|
116
|
-
pending_dataset_decl = var_name
|
|
117
|
-
|
|
118
|
-
# 同行 RHS 消费检测:DataSet result = ds1.union(ds2)
|
|
119
|
-
eq_idx = code_line.find("=", code_line.find(var_name))
|
|
120
|
-
if eq_idx >= 0:
|
|
121
|
-
rhs = code_line[eq_idx + 1:]
|
|
122
|
-
for tracked_var in list(dataset_vars):
|
|
123
|
-
if tracked_var != var_name and re.search(
|
|
124
|
-
rf"\b{re.escape(tracked_var)}\b", rhs
|
|
125
|
-
):
|
|
126
|
-
consumed_vars.add(tracked_var)
|
|
127
|
-
# 派生链传递:如果源被管理(在 try 块或已被标记为消费),派生变量同享管理
|
|
128
|
-
if tracked_var in closed_vars or tracked_var in consumed_vars:
|
|
129
|
-
consumed_vars.add(var_name)
|
|
130
|
-
else:
|
|
131
|
-
# ── 跨行消费检测:上一行 DataSet xxx = ds1,本行 .union(ds2) ──
|
|
132
|
-
if pending_dataset_decl and code_line.strip().startswith("."):
|
|
133
|
-
for tracked_var in list(dataset_vars):
|
|
134
|
-
if tracked_var != pending_dataset_decl and re.search(
|
|
135
|
-
rf"\b{re.escape(tracked_var)}\b", code_line
|
|
136
|
-
):
|
|
137
|
-
consumed_vars.add(tracked_var)
|
|
138
|
-
if tracked_var in closed_vars or tracked_var in consumed_vars:
|
|
139
|
-
consumed_vars.add(pending_dataset_decl)
|
|
140
|
-
else:
|
|
141
|
-
pending_dataset_decl = None
|
|
142
|
-
|
|
143
|
-
# ── try-with-resources 检测 ──
|
|
144
|
-
if re.search(r"\btry\s*\(", code_line):
|
|
145
|
-
in_try_resource = True
|
|
146
|
-
|
|
147
|
-
if in_try_resource:
|
|
148
|
-
try_paren_depth += code_line.count("(") - code_line.count(")")
|
|
149
|
-
for vn in DATASET_VAR_DECL_PATTERN.findall(code_line):
|
|
150
|
-
closed_vars.add(vn)
|
|
151
|
-
if try_paren_depth <= 0:
|
|
152
|
-
in_try_resource = False
|
|
153
|
-
try_paren_depth = 0
|
|
154
|
-
|
|
155
|
-
# ── .close() 检测 ──
|
|
156
|
-
for close_var in DATASET_CLOSE_PATTERN.findall(code_line):
|
|
157
|
-
closed_vars.add(close_var)
|
|
158
|
-
|
|
159
|
-
# ── return 消费检测 ──
|
|
160
|
-
return_match = DATASET_RETURN_PATTERN.search(code_line)
|
|
161
|
-
if return_match and return_match.group(1) in dataset_vars:
|
|
162
|
-
consumed_vars.add(return_match.group(1))
|
|
163
|
-
|
|
164
|
-
# ── 汇总本方法的 RESOURCE-004 ──
|
|
165
|
-
for var_name, decl_line in dataset_vars.items():
|
|
166
|
-
if var_name in closed_vars or var_name in consumed_vars:
|
|
167
|
-
continue
|
|
168
|
-
issues.append(LintIssue(
|
|
169
|
-
file=filepath,
|
|
170
|
-
line=decl_line,
|
|
171
|
-
severity=Severity.ERROR,
|
|
172
|
-
rule_id="RESOURCE-004",
|
|
173
|
-
message=f"检测到 DataSet 变量 `{var_name}` 但未发现 close() 调用,也未被 return 或后续 DataSet 计算消费",
|
|
174
|
-
fix_hint="在最靠近使用完成的位置调用 close();若该 DataSet 是方法返回值或已参与计算/合并则无需关闭",
|
|
175
|
-
source_line=lines[decl_line - 1].strip(),
|
|
176
|
-
))
|