kcode-pi 0.1.6 → 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.
Files changed (47) hide show
  1. package/README.md +18 -2
  2. package/package.json +1 -1
  3. package/src/official/kingdee-skills.ts +60 -13
  4. package/src/rules/checker.ts +143 -0
  5. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/SKILL.md +2 -2
  6. package/vendor/kingdee-skills/ok-cosmic/SKILL.md +52 -101
  7. package/vendor/kingdee-skills/ok-cosmic/agents/openai.yaml +4 -4
  8. package/vendor/kingdee-skills/ok-cosmic/manifest.json +21 -20
  9. package/vendor/kingdee-skills/ok-cosmic/ok-cosmic-intro.html +1 -1
  10. package/vendor/kingdee-skills/ok-cosmic/rules/a-layer-rules.json +1 -1
  11. package/vendor/kingdee-skills/ok-cosmic/rules/anti-patterns.md +2 -2
  12. package/vendor/kingdee-skills/ok-cosmic/rules/coding-preferences.md +4 -4
  13. package/vendor/kingdee-skills/ok-cosmic/rules/constraints.md +3 -3
  14. package/vendor/kingdee-skills/ok-cosmic/rules/decision-matrix.md +8 -8
  15. package/vendor/kingdee-skills/ok-cosmic/rules/intent-routing.md +1 -1
  16. package/vendor/kingdee-skills/ok-cosmic/rules/post-check.md +19 -18
  17. package/vendor/kingdee-skills/ok-ksql/SKILL.md +9 -9
  18. package/vendor/kingdee-skills/ok-ksql/manifest.json +2 -1
  19. package/vendor/kingdee-skills/ok-ksql/references/ksql-datafix.md +2 -2
  20. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/pattern-matcher.py +0 -336
  21. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/review-score-calculator.py +0 -121
  22. package/vendor/kingdee-skills/ok-cosmic/CHANGELOG.md +0 -295
  23. package/vendor/kingdee-skills/ok-cosmic/README.md +0 -460
  24. package/vendor/kingdee-skills/ok-cosmic/requirements.txt +0 -2
  25. package/vendor/kingdee-skills/ok-cosmic/scripts/config_loader.py +0 -204
  26. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-api-knowledge.py +0 -910
  27. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-basedata-query.py +0 -359
  28. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-config-check.py +0 -181
  29. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-extpoints-query.py +0 -389
  30. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-form-metadata.py +0 -856
  31. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-check.py +0 -262
  32. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-lint.py +0 -293
  33. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/__init__.py +0 -2
  34. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/base.py +0 -393
  35. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/resource_check.py +0 -176
  36. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/scene_check.py +0 -375
  37. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/style_check.py +0 -434
  38. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/verify_check.py +0 -36
  39. package/vendor/kingdee-skills/ok-cosmic/scripts/route_client.py +0 -186
  40. package/vendor/kingdee-skills/ok-cosmic/scripts/script_utils.py +0 -40
  41. package/vendor/kingdee-skills/ok-cosmic/scripts/sqlite_cache.py +0 -142
  42. package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-commons.jar +0 -0
  43. package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-features.jar +0 -0
  44. package/vendor/kingdee-skills/ok-cosmic/setup/setup-mac.sh +0 -18
  45. package/vendor/kingdee-skills/ok-cosmic/setup/setup-windows.bat +0 -53
  46. package/vendor/kingdee-skills/ok-cosmic/setup/setup.jar +0 -0
  47. 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
- ))