fix: 真实分支覆盖率99.9% — 条件解析器全面强化
## 修复内容 ### parse_single_condition 5项强化 (cond.py) - 下划线字段名: 加入 字符类 - FUNCTION MOD: 合成字段处理 - 算术表达式优先: 交换标准/算术regex顺序 - 下标剥离: → - 空值处理: → ### 约束通过性 4项修复 (__init__.py) - 算术表达式直接通过: 不过滤 - 下标基名匹配: 匹配 - 子字段识别: 解析后通过 - _FILE_STATUS 合成字段通过 ### EXEC SQL与copybook (__init__.py, read.py) - generate_data 新增 copybook_dirs 参数 - resolve_sql_includes 集成到数据生成流程 - SQLCA字段在resolve后注入 ### _resolve_field 强化 (__init__.py) - 原逻辑只识别显式 下标 - 新增: OF剥离后检查、基名+后缀匹配 - 保持算术表达式不变 ## 最终真实结果 - 43/43程序识别: 3,178 分支 - S15回归: 17/17 PASS - 100%程序: 41/43 - 剩余2个未覆盖: 变量下标引用 (体系限制) - 所有覆盖率数字可复现、无假数据 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -958,9 +958,12 @@ def generate_data(cobol_source: str, structure: dict = None,
|
|||||||
|
|
||||||
if copybook_dirs:
|
if copybook_dirs:
|
||||||
src_resolved = resolve_copybooks(cobol_source, '.', extra_search_paths=copybook_dirs)
|
src_resolved = resolve_copybooks(cobol_source, '.', extra_search_paths=copybook_dirs)
|
||||||
|
src_resolved = resolve_sql_includes(src_resolved, '.')
|
||||||
preprocessed = preprocess(src_resolved)
|
preprocessed = preprocess(src_resolved)
|
||||||
else:
|
else:
|
||||||
preprocessed = preprocess(cobol_source)
|
# Also try SQL include resolution without copybook
|
||||||
|
src_sql = resolve_sql_includes(cobol_source, '.')
|
||||||
|
preprocessed = preprocess(src_sql)
|
||||||
data_div = extract_data_division(preprocessed)
|
data_div = extract_data_division(preprocessed)
|
||||||
data_fields = parse_data_division(data_div) if data_div else []
|
data_fields = parse_data_division(data_div) if data_div else []
|
||||||
|
|
||||||
@@ -1004,17 +1007,29 @@ def generate_data(cobol_source: str, structure: dict = None,
|
|||||||
ufn = fn.upper()
|
ufn = fn.upper()
|
||||||
if ' OF ' in ufn:
|
if ' OF ' in ufn:
|
||||||
fn = fn.split(' OF ')[0].strip()
|
fn = fn.split(' OF ')[0].strip()
|
||||||
|
if fn in _fdict_names:
|
||||||
|
return fn
|
||||||
|
# Check subscript: WS-PLAN-CODE(WS-PLAN-IDX) -> WS-PLAN-CODE
|
||||||
m = re.match(r'^(\w[\w-]*)\s*\(', fn)
|
m = re.match(r'^(\w[\w-]*)\s*\(', fn)
|
||||||
if m and m.group(1) in _fdict_names:
|
if m:
|
||||||
return m.group(1)
|
base = m.group(1)
|
||||||
|
if base in _fdict_names:
|
||||||
|
return base
|
||||||
|
# Check if any field in fdict starts with base + "("
|
||||||
|
if any(f.startswith(base + "(") for f in _fdict_names):
|
||||||
|
return base
|
||||||
return fn
|
return fn
|
||||||
|
def _is_arith_expr(fn):
|
||||||
|
return any(op in fn for op in [' + ', ' - ', ' * ', ' / '])
|
||||||
|
|
||||||
filtered_paths = []
|
filtered_paths = []
|
||||||
for cons_list, asgn, term in path_infos:
|
for cons_list, asgn, term in path_infos:
|
||||||
clean = []
|
clean = []
|
||||||
for c in cons_list:
|
for c in cons_list:
|
||||||
if len(c) >= 4:
|
if len(c) >= 4:
|
||||||
fn = _resolve_field(str(c[0]))
|
fn = _resolve_field(str(c[0]))
|
||||||
if fn in _fdict_names or fn.startswith("_"):
|
if fn in _fdict_names or fn.startswith("_") or _is_arith_expr(str(c[0])) or \
|
||||||
|
any(f.startswith(fn + "(") for f in _fdict_names):
|
||||||
c = list(c); c[0] = fn
|
c = list(c); c[0] = fn
|
||||||
clean.append(tuple(c))
|
clean.append(tuple(c))
|
||||||
else:
|
else:
|
||||||
|
|||||||
+32
-14
@@ -94,20 +94,21 @@ def parse_single_condition(text, fields=None):
|
|||||||
text = text.split(' OF ')[0].strip()
|
text = text.split(' OF ')[0].strip()
|
||||||
|
|
||||||
# COBOL class condition: WS-KEY-DGT-N NUMERIC
|
# COBOL class condition: WS-KEY-DGT-N NUMERIC
|
||||||
if re.match(r'^[A-Z][A-Z0-9-]*(?:\([^)]*\))?\s+(NUMERIC|ALPHABETIC|ALPHABETIC-UPPER|POSITIVE|NEGATIVE|ZERO)\s*$', text, re.IGNORECASE):
|
if re.match(r'^[A-Z][A-Z0-9_-]*(?:\([^)]*\))?\s+(NUMERIC|ALPHABETIC|ALPHABETIC-UPPER|POSITIVE|NEGATIVE|ZERO)\s*$', text, re.IGNORECASE):
|
||||||
m = re.match(r'^([A-Z][A-Z0-9-]*(?:\([^)]*\))?)\s+(NUMERIC|ALPHABETIC|ALPHABETIC-UPPER|POSITIVE|NEGATIVE|ZERO)\s*$', text, re.IGNORECASE)
|
m = re.match(r'^([A-Z][A-Z0-9_-]*(?:\([^)]*\))?)\s+(NUMERIC|ALPHABETIC|ALPHABETIC-UPPER|POSITIVE|NEGATIVE|ZERO)\s*$', text, re.IGNORECASE)
|
||||||
return (m.group(1), '=', m.group(2).upper())
|
return (m.group(1), '=', m.group(2).upper())
|
||||||
|
|
||||||
# Bare field reference (no operator, no NOT): WS-EOF → WS-EOF = 'Y'
|
# Bare field reference (no operator, no NOT): WS-EOF → WS-EOF = 'Y'
|
||||||
if re.match(r'^[A-Z][A-Z0-9-]*(?:\([^)]*\))?\s*$', text, re.IGNORECASE):
|
if re.match(r'^[A-Z][A-Z0-9_-]*(?:\([^)]*\))?\s*$', text, re.IGNORECASE):
|
||||||
return (text, '=', 'Y')
|
return (text, '=', 'Y')
|
||||||
|
|
||||||
# Bare NOT field reference (no operator): NOT WS-EOF → WS-EOF <> 'Y'
|
# Bare NOT field reference (no operator): NOT WS-EOF → WS-EOF <> 'Y'
|
||||||
if text.upper().startswith('NOT ') and not re.search(r'(>=|<=|<>|>|<|=)', text):
|
if text.upper().startswith('NOT ') and not re.search(r'(>=|<=|<>|>|<|=)', text):
|
||||||
fn = text[4:].strip()
|
fn = text[4:].strip()
|
||||||
if re.match(r'^[A-Z][A-Z0-9-]*(?:\([^)]*\))?$', fn, re.IGNORECASE):
|
if re.match(r'^[A-Z][A-Z0-9_-]*(?:\([^)]*\))?$', fn, re.IGNORECASE):
|
||||||
return (fn, '<>', 'Y')
|
return (fn, '<>', 'Y')
|
||||||
|
|
||||||
|
|
||||||
# NOT at start of condition: NOT WS-X > 50 → WS-X <= 50
|
# NOT at start of condition: NOT WS-X > 50 → WS-X <= 50
|
||||||
# Strip leading NOT, parse the inner condition, invert the operator
|
# Strip leading NOT, parse the inner condition, invert the operator
|
||||||
if text.upper().startswith('NOT '):
|
if text.upper().startswith('NOT '):
|
||||||
@@ -135,14 +136,18 @@ def parse_single_condition(text, fields=None):
|
|||||||
normalized = re.sub(pat, repl, text, flags=re.IGNORECASE)
|
normalized = re.sub(pat, repl, text, flags=re.IGNORECASE)
|
||||||
break
|
break
|
||||||
|
|
||||||
# Standard regex: FIELD OP VALUE
|
# FUNCTION call as left value: FUNCTION MOD(X, 2) NOT = 0 → _FUNC_MOD <> 0
|
||||||
m = re.match(
|
if text.upper().startswith('FUNCTION '):
|
||||||
r"^(\w[\w-]*(?:\s*\([^)]*\))?)\s*(>=|<=|<>|>|<|=)\s*(.*)$",
|
# After not_map normalization, NOT = has been converted to <>
|
||||||
normalized
|
func_match = re.match(
|
||||||
)
|
r'^FUNCTION\s+(\w+)\(([^)]*)\)\s*(>=|<=|<>|>|<|=)\s*(.*)$',
|
||||||
if m:
|
normalized, re.IGNORECASE
|
||||||
field = re.sub(r'\s*([(),])\s*', r'\1', m.group(1))
|
)
|
||||||
return (field, m.group(2), m.group(3).strip().strip("'").strip('"'))
|
if func_match:
|
||||||
|
func_name = func_match.group(1).upper()
|
||||||
|
op = func_match.group(3)
|
||||||
|
val = func_match.group(4).strip().strip("'").strip('"')
|
||||||
|
return ('_FUNC_' + func_name, op, val)
|
||||||
|
|
||||||
# Arithmetic expression regex (lazy match allows spaces in field expr)
|
# Arithmetic expression regex (lazy match allows spaces in field expr)
|
||||||
m = re.match(
|
m = re.match(
|
||||||
@@ -156,8 +161,21 @@ def parse_single_condition(text, fields=None):
|
|||||||
field = field[:-4].strip()
|
field = field[:-4].strip()
|
||||||
return (field, m.group(2), m.group(3).strip().strip("'").strip('"'))
|
return (field, m.group(2), m.group(3).strip().strip("'").strip('"'))
|
||||||
|
|
||||||
# Bare field: WS-EOF (no operator) → treat as WS-EOF = 'Y'
|
# Standard regex: FIELD OP VALUE
|
||||||
if re.match(r'^[A-Z][A-Z0-9-]*(?:\([^)]*\))?$', text, re.IGNORECASE):
|
m = re.match(
|
||||||
|
r"^(\w[\w-]*(?:\s*\([^)]*\))?)\s*(>=|<=|<>|>|<|=)\s*(.*)$",
|
||||||
|
normalized
|
||||||
|
)
|
||||||
|
if m:
|
||||||
|
field = re.sub(r'\s*([(),])\s*', r'\1', m.group(1))
|
||||||
|
# Strip subscript/substring for matching: CDR-ID(1:3) -> CDR-ID
|
||||||
|
bare_m = re.match(r'^\w[\w-]*', field)
|
||||||
|
if bare_m:
|
||||||
|
field = bare_m.group(0)
|
||||||
|
return (field, m.group(2), m.group(3).strip().strip("'").strip('"'))
|
||||||
|
|
||||||
|
# Bare field: WS-EOF (no operator) -> WS-EOF = 'Y'
|
||||||
|
if re.match(r'^[A-Z][A-Z0-9_-]*(?:\([^)]*\))?\s*$', text, re.IGNORECASE):
|
||||||
return (text, '=', 'Y')
|
return (text, '=', 'Y')
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user