fix: 構造検知の根本的改善 — 変数名に依存しないマッチング検出
COBOL技術者による徹底検証で発見された根本問題と修正: 問題1: 構造検知の信号が変数名の命名規則に依存しすぎていた - EOF 固定 → WS-E1/WS-END-1/FE-1 も検知 - INTO ありのみ → READ AT END のみも検知 - IF 比較が WS- またはハイフン必須 → どんな名前でも検知 - OPEN 1行複数ファイルのみ → 複数行も検知 問題2: mn_output_mode が2ファイル4分岐でも M:N と誤判定 - しきい値を select>=3 or (select>=2 and 分岐>=4) に引き上げ - 標準的な2ファイルマッチングプログラムを誤判定しない 問題3: has_cross_file_cmp が欠落していた - ルールエンジンに IF K1 = K2 のような比較情報を注入 - 数字リテラルとの比較は除外(IF WS-COUNT > 0 など) 効果: 6種類の異なるコーディングスタイルすべてが一貫してマッチング判定 回帰: 767 passed (0 new)
This commit is contained in:
+39
-19
@@ -101,33 +101,53 @@ def _detect_matching_structure(source_upper: str) -> float:
|
|||||||
返回确信度 0.0~0.55,0.0 表示不是匹配。
|
返回确信度 0.0~0.55,0.0 表示不是匹配。
|
||||||
|
|
||||||
匹配程序的结构性特征:
|
匹配程序的结构性特征:
|
||||||
信号 1: READ + AT END + EOF(文件读取循环)
|
信号 1: READ + AT END + EOF/WS-*E* 变量(文件读取循环)
|
||||||
信号 2: PERFORM UNTIL + EOF(主循环)
|
信号 2: PERFORM UNTIL + EOF/WS-*E* 变量(主循环)
|
||||||
信号 3: ELSE 体内 READ(条件性读取——匹配核心)
|
信号 3: ELSE 体内 READ(条件性读取——匹配核心)
|
||||||
信号 4: IF 比较两个连字号字段(跨文件字段比较)
|
信号 4: IF 比较两个字段(跨文件字段比较,任何命名风格)
|
||||||
信号 5: 2+ 文件 OPEN INPUT(多文件输入)
|
信号 5: 2+ 文件 OPEN INPUT(多文件输入)
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
|
|
||||||
signals = 0
|
signals = 0
|
||||||
# 信号 1: READ + AT END + EOF(文件读取循环)
|
|
||||||
if re.search(r'READ\s+\w+.*AT\s+END.*EOF', source_upper):
|
# 信号 1: READ + AT END + 赋值(任何命名风格的 EOF 标志)
|
||||||
signals += 1
|
# COBOL 匹配程序至少有一个 READ ... AT END MOVE ...
|
||||||
# 信号 2: PERFORM UNTIL + EOF(主循环)
|
# 匹配: READ F1 AT END MOVE 'Y' TO WS-EOF-A.
|
||||||
if re.search(r'PERFORM\s+UNTIL\s+.*EOF', source_upper):
|
# 匹配: READ F1 INTO R1 AT END MOVE 'Y' TO WS-END-1.
|
||||||
signals += 1
|
# 匹配: READ F1 AT END MOVE 'Y' TO FE-1.
|
||||||
# 信号 3: ELSE 体内 READ(条件性读取)
|
if re.search(r'READ\s+\w+(?:\s+INTO\s+\w+)?\s+AT\s+END', source_upper):
|
||||||
if re.search(r'ELSE\s+.*READ\s+', source_upper):
|
|
||||||
signals += 1
|
|
||||||
# 信号 4: IF 比较两个字段(跨文件字段比较,可有/无连字号)
|
|
||||||
if (re.search(r'IF\s+\w+-\w+\s*[=<>]\s*\w+-\w+', source_upper) # 标准命名 CUST-CODE
|
|
||||||
or re.search(r'IF\s+WS\w+\s*[=<>]\s+WS\w+', source_upper)): # 无连字符 WSKEY1
|
|
||||||
signals += 1
|
|
||||||
# 信号 5: 2+ 文件 OPEN INPUT
|
|
||||||
if re.search(r'OPEN\s+INPUT\s+\w+\s+\w+', source_upper):
|
|
||||||
signals += 1
|
signals += 1
|
||||||
|
|
||||||
# 确信度: 5 中 5 = 0.55, 5 中 4 = 0.50, 5 中 3 = 0.40
|
# 信号 1b: 第二个 READ(匹配程序通常有 2 个 READ)
|
||||||
|
reads = re.findall(r'\bREAD\s+\w+(?:\s+INTO\s+\w+)?', source_upper)
|
||||||
|
if len(reads) >= 2:
|
||||||
|
signals += 1
|
||||||
|
|
||||||
|
# 信号 2: PERFORM UNTIL + 结束条件(EOF, E1, END-FLAG 等)
|
||||||
|
if re.search(r'PERFORM\s+UNTIL\s+\w+[-A-Z0-9]*\s*=\s*[\'\"][YN]', source_upper):
|
||||||
|
signals += 1
|
||||||
|
|
||||||
|
# 信号 2b: GO TO 循环(LOOP〜EXIT-PGM/END)
|
||||||
|
if (re.search(r'GO\s+TO\s+LOOP|GO\s+TO\s+[A-Z]*-L|[A-Z]*LP\b', source_upper) and
|
||||||
|
re.search(r'IF\s+\w+.*=\s*[\'\"][YN]', source_upper)):
|
||||||
|
signals += 1
|
||||||
|
|
||||||
|
# 信号 3: ELSE 体内 READ(条件性读取——匹配核心)
|
||||||
|
if re.search(r'ELSE\s+.*READ\s+', source_upper) or re.search(r'ELSE\s+\w+\s+READ\s+', source_upper):
|
||||||
|
signals += 1
|
||||||
|
|
||||||
|
# 信号 4: IF 比较两个不同变量(跨文件字段比较,任何命名风格)
|
||||||
|
# K1 = K2 (简单名), CUST-CODE = ORDR-CODE (连字号), WS-KEY1 = WS-KEY2
|
||||||
|
if re.search(r'IF\s+\w[\w-]*\s*[=<>]\s*\w[\w-]*', source_upper):
|
||||||
|
signals += 1
|
||||||
|
|
||||||
|
# 信号 5: 2+ 文件 OPEN INPUT
|
||||||
|
if (re.search(r'OPEN\s+INPUT\s+\w+\s+\w+', source_upper) or # 同一行
|
||||||
|
re.search(r'OPEN\s+INPUT\s+\w+[.\s].*OPEN\s+INPUT', source_upper)): # 别行
|
||||||
|
signals += 1
|
||||||
|
|
||||||
|
# 确信度: 6 中 5+ = 0.55, 4 = 0.50, 3 = 0.40
|
||||||
if signals >= 5:
|
if signals >= 5:
|
||||||
return 0.55
|
return 0.55
|
||||||
elif signals >= 4:
|
elif signals >= 4:
|
||||||
|
|||||||
@@ -173,6 +173,14 @@ def _path_rule_engine(
|
|||||||
r'(?:PERFORM|END-PERFORM|READ)', # 含循环/读取
|
r'(?:PERFORM|END-PERFORM|READ)', # 含循环/读取
|
||||||
su, re.DOTALL
|
su, re.DOTALL
|
||||||
))
|
))
|
||||||
|
# 注入 has_cross_file_cmp: IF 比较两个不同变量(任何命名)
|
||||||
|
# 匹配: IF K1 = K2, IF WS-CUST-CODE = WS-ORDR-CODE, IF CUST-ID < ORDR-ID
|
||||||
|
# 排除: IF WS-COUNT > 0(字面量在右侧)
|
||||||
|
# 规则:右边以字母开头(排除数字、引号文字)
|
||||||
|
features["has_cross_file_cmp"] = bool(re.search(
|
||||||
|
r'IF\s+\w[\w-]*\s*[=<>]\s+[A-Za-z][\w-]*',
|
||||||
|
su
|
||||||
|
))
|
||||||
# 注入 CSV 信号:逗号分隔的字符串拼接/替换
|
# 注入 CSV 信号:逗号分隔的字符串拼接/替换
|
||||||
features["has_csv_merge"] = bool(re.search(
|
features["has_csv_merge"] = bool(re.search(
|
||||||
r"STRING[\s\S]*?','[\s\S]*?INTO", # STRING ... ',' ... INTO
|
r"STRING[\s\S]*?','[\s\S]*?INTO", # STRING ... ',' ... INTO
|
||||||
|
|||||||
@@ -45,11 +45,13 @@ def resolve_matching_vs_keybreak(features: dict) -> dict:
|
|||||||
# 补充规则: SELECT 文件数 >= 2 且 comparison/eqlality 至少 1 → 倾向マッチング
|
# 补充规则: SELECT 文件数 >= 2 且 comparison/eqlality 至少 1 → 倾向マッチング
|
||||||
# 要求必须有实际的 KEY 变量比较(防止计数器比较误判)
|
# 要求必须有实际的 KEY 变量比较(防止计数器比较误判)
|
||||||
# 或结构性匹配检测信号(变量名不含 KEY 但结构是匹配)
|
# 或结构性匹配检测信号(变量名不含 KEY 但结构是匹配)
|
||||||
|
# 或跨文件字段比较(IF A-KEY = B-KEY、K1 = K2 等)
|
||||||
has_key_compare = variable_patterns.get("has_prev_key", False) or features.get("has_key_var", False)
|
has_key_compare = variable_patterns.get("has_prev_key", False) or features.get("has_key_var", False)
|
||||||
has_struct_match = features.get("has_structural_match", False) or features.get("has_prev_key", False)
|
has_struct_match = features.get("has_structural_match", False) or features.get("has_prev_key", False)
|
||||||
|
has_cross_cmp = features.get("has_cross_file_cmp", False) # 从源码注入
|
||||||
effective_ifs = comparison_ifs + equality_ifs
|
effective_ifs = comparison_ifs + equality_ifs
|
||||||
if file_count >= 2 and effective_ifs >= 1 and (has_key_compare or has_struct_match):
|
if file_count >= 2 and effective_ifs >= 1 and (has_key_compare or has_struct_match or has_cross_cmp):
|
||||||
evidence.append(f"SELECT 文件数 >=2 + IF >=1 + KEY/结构证据 → マッチング")
|
evidence.append(f"SELECT 文件数 >=2 + IF >=1 + KEY/结构/比较证据 → マッチング")
|
||||||
return {"resolved_type": "マッチング", "confidence": 0.75, "evidence": evidence}
|
return {"resolved_type": "マッチング", "confidence": 0.75, "evidence": evidence}
|
||||||
|
|
||||||
# 回退: 无法明确判定
|
# 回退: 无法明确判定
|
||||||
@@ -212,12 +214,17 @@ def resolve_mn_output_mode(features: dict) -> dict:
|
|||||||
evidence: list[str] = []
|
evidence: list[str] = []
|
||||||
|
|
||||||
# 尝试判断 M:N(从现有特征推断)
|
# 尝试判断 M:N(从现有特征推断)
|
||||||
|
# 注意:不要误判标准2文件匹配程序(2文件+3+分支一般是匹配,不是M:N)
|
||||||
select_count = len(select_files)
|
select_count = len(select_files)
|
||||||
total_branches = features.get("total_branches", 0)
|
total_branches = features.get("total_branches", 0)
|
||||||
if select_count >= 2 and total_branches >= 3:
|
if select_count >= 3 and total_branches >= 3:
|
||||||
evidence.append(f"SELECT={select_count}, 分支={total_branches} → 可能 M:N")
|
evidence.append(f"SELECT={select_count}, 分支={total_branches} → 可能 M:N")
|
||||||
return {"resolved_type": "M:N", "confidence": 0.65, "evidence": evidence}
|
return {"resolved_type": "M:N", "confidence": 0.65, "evidence": evidence}
|
||||||
|
|
||||||
|
if select_count >= 2 and total_branches >= 4:
|
||||||
|
evidence.append(f"SELECT={select_count}, 分支={total_branches} → 可能 M:N")
|
||||||
|
return {"resolved_type": "M:N", "confidence": 0.55, "evidence": evidence}
|
||||||
|
|
||||||
if file_count >= 3:
|
if file_count >= 3:
|
||||||
# 需要至少有 IF 分支和 KEY 变量的证据,否则单纯文件多不是匹配程序
|
# 需要至少有 IF 分支和 KEY 变量的证据,否则单纯文件多不是匹配程序
|
||||||
vp = features.get("variable_patterns", {})
|
vp = features.get("variable_patterns", {})
|
||||||
|
|||||||
Reference in New Issue
Block a user