feat: SQL between/hostvar-key alignment, class-condition parsing, gcov merge across scenario runs

This commit is contained in:
hangshuo652
2026-08-09 17:43:00 +08:00
parent f331c8fa2a
commit 273a3f8211
31 changed files with 3789 additions and 272 deletions
+147 -11
View File
@@ -161,9 +161,17 @@ class _BrParser:
return self.lines[self.pos].strip()
return ''
def peek_next(self):
if self.pos + 1 < len(self.lines):
return self.lines[self.pos + 1].strip()
return ''
def clean(self):
return self.peek().rstrip('.').strip()
def clean_next(self):
return self.peek_next().rstrip('.').strip()
def advance(self):
self.pos += 1
@@ -217,6 +225,38 @@ class _BrParser:
if perf_node:
seq.add(perf_node)
continue
m_sm = re.match(r'^(MERGE|SORT)\s+(\w[\w-]*)\s*$', line)
if m_sm:
sort_file = m_sm.group(2).strip()
self.advance()
# 收集 MERGE/SORT 语句的续行(直到以 . 结尾)
stmt_parts = [line]
while self.pos < len(self.lines):
raw = self.peek()
stmt_parts.append(self.clean())
self.advance()
# clean() 会剥离末尾句点,须用原始行判断语句终止;
# 否则该循环永不退出,吞掉 SORT/MERGE 后的全部语句。
if raw.rstrip().endswith('.'):
break
stmt_text = ' '.join(stmt_parts)
# 提取 INPUT/OUTPUT PROCEDURE 段名并内联(如同 PERFORM 段)
for pm in re.finditer(
r'\b(?:INPUT|OUTPUT)\s+PROCEDURE\s+(\w[\w-]*)',
stmt_text, re.IGNORECASE
):
sec = pm.group(1).strip().upper()
if sec in self.paragraphs:
start, end = self.paragraphs[sec]
para_lines = self.raw_lines[start:end + 1]
sub = _BrParser(
[l for l in para_lines if l.strip()],
self.paragraphs, self.raw_lines, self.assignments, self.fields
)
sub_seq = sub.parse_seq()
for child in sub_seq.children:
seq.add(child)
continue
m_search = re.match(r'^SEARCH\b(?:\s+(ALL))?\s+(\w[\w-]*)(?:\s+VARYING\s+(\w[\w-]*))?', line, re.IGNORECASE)
if m_search:
seq.add(self._parse_search(m_search))
@@ -277,6 +317,23 @@ class _BrParser:
break
self.advance()
continue
m = re.match(r'^READ\s+(\w[\w-]*)\s*$', line, re.IGNORECASE)
if m and self.peek_next().startswith('INTO'):
tgt = self.clean_next().replace('INTO', '', 1).strip().upper()
if re.match(r'^\w[\w-]*$', tgt):
info = {'type': 'read_into', 'file': m.group(1).strip().upper(), 'source_vars': []}
self.assignments.setdefault(tgt, []).append(info)
seq.add(Assign(tgt, info))
self.advance()
self.advance()
# 跳过 READ 语句剩余行(AT END / NOT AT END / END-READ
while self.pos < len(self.lines):
cl = self.clean()
if cl in ('END-READ', 'END-READ.'):
self.advance()
break
self.advance()
continue
m_set_false = re.match(r'^SET\s+(\w[\w-]*)\s+TO\s+FALSE\s*$', line, re.IGNORECASE)
if m_set_false:
seq.add(self._parse_set_false(m_set_false.group(1)))
@@ -796,6 +853,16 @@ class _BrParser:
def _parse_perform(self):
line = self.clean()
# PERFORM WITH TEST AFTER/BEFORE UNTIL ...(可换行)
m = re.match(r'^PERFORM\s+(?:WITH\s+TEST\s+(?:AFTER|BEFORE)\s+)?UNTIL\s+(.+?)\s*$', line)
if m:
node = BrPerform('until', condition=m.group(1).strip())
self.advance()
node.body_seq = self.parse_seq(end_check=lambda l: l == 'END-PERFORM')
if self.clean() == 'END-PERFORM':
self.advance()
return node
m = re.match(r'^PERFORM\s+UNTIL\s+(.+?)\s*$', line)
if m:
node = BrPerform('until', condition=m.group(1).strip())
@@ -965,6 +1032,28 @@ class _BrParser:
self._inline_perform(node, target)
return node
# PERFORM WITH TEST AFTER/BEFOREUNTIL 在下一行)
m = re.match(r'^PERFORM\s+WITH\s+TEST\s+(?:AFTER|BEFORE)\s*$', line)
if m:
save_pos = self.pos
condition = None
self.advance()
if self.pos < len(self.lines):
nxt = self.clean()
um = re.match(r'^UNTIL\s+(.+)$', nxt)
if um:
condition = um.group(1).strip()
self.advance()
if condition:
node = BrPerform('until', condition=condition)
node.body_seq = self.parse_seq(end_check=lambda l: l == 'END-PERFORM')
if self.clean() == 'END-PERFORM':
self.advance()
return node
self.pos = save_pos
self.advance()
return None
self.advance()
return None
@@ -1189,24 +1278,24 @@ class _BrParser:
_RE_SELECT_INTO = re.compile(
r'SELECT\s+(.*?)\s+INTO\s+(:\w[\w-]*(?:\s*,\s*:\w[\w-]*(?::\w[\w-]*)?)*)'
r'\s+FROM\s+(\w[\w-]*)',
r'\s+FROM\s+([\w-]+(?:\.[\w-]+)?)',
re.IGNORECASE
)
_RE_WHERE = re.compile(r'\bWHERE\b\s+(.*)', re.IGNORECASE)
_RE_SQL_INSERT = re.compile(
r'INSERT\s+INTO\s+(\w[\w-]*)\s*\(([^)]+)\)\s+VALUES\s*\(([^)]+)\)',
r'INSERT\s+INTO\s+([\w-]+(?:\.[\w-]+)?)\s*\(([^)]+)\)\s+VALUES\s*\(([^)]+)\)',
re.IGNORECASE
)
_RE_SQL_DELETE = re.compile(
r'DELETE\s+FROM\s+(\w[\w-]*)(?:\s+WHERE\s+(.+))?',
r'DELETE\s+FROM\s+([\w-]+(?:\.[\w-]+)?)(?:\s+WHERE\s+(.+))?',
re.IGNORECASE
)
_RE_SQL_UPDATE = re.compile(
r'UPDATE\s+(\w[\w-]*)\s+SET\s+(.+?)(?:\s+WHERE\s+(.+))?$',
r'UPDATE\s+([\w-]+(?:\.[\w-]+)?)\s+SET\s+(.+?)(?:\s+WHERE\s+(.+))?$',
re.IGNORECASE
)
@@ -1229,7 +1318,7 @@ class _BrParser:
result = re.sub(r'\s+', ' ', result)
return result
def _parse_sql(self, sql_text: str):
def _parse_sql(self, sql_text: str, pos: int = None):
"""Parse SQL text from EXEC SQL block. Returns Assign node or None."""
# 1) SELECT ... INTO ... FROM
m = self._RE_SELECT_INTO.search(sql_text)
@@ -1259,6 +1348,7 @@ class _BrParser:
'into_vars': into_vars,
'where': where_clause,
'sql_text': sql_text,
'pos': pos,
}
for var in into_vars:
@@ -1288,6 +1378,7 @@ class _BrParser:
'raw_values': values_str,
'host_vars': host_vars,
'sql_text': sql_text,
'pos': pos,
}
synthetic = f'__SQL_INSERT_{table}'
self.assignments.setdefault(synthetic, []).append(info)
@@ -1307,6 +1398,7 @@ class _BrParser:
'where': where_clause,
'host_vars': [h.upper() for h in host_vars],
'sql_text': sql_text,
'pos': pos,
}
synthetic = f'__SQL_DELETE_{table}'
self.assignments.setdefault(synthetic, []).append(info)
@@ -1314,7 +1406,7 @@ class _BrParser:
# 4a) DECLARE CURSOR ... FOR SELECT ... FROM ...
m = re.search(
r'DECLARE\s+(\w[\w-]*)\s+CURSOR\s+FOR\s+SELECT\s+(.*?)\s+FROM\s+(\w[\w-]*)\s*(.*)',
r'DECLARE\s+(\w[\w-]*)\s+CURSOR\s+FOR\s+SELECT\s+(.*?)\s+FROM\s+([\w-]+(?:\.[\w-]+)?)\s*(.*)',
sql_text, re.IGNORECASE
)
if m:
@@ -1336,6 +1428,7 @@ class _BrParser:
'into_vars': [],
'where': where_clause,
'sql_text': sql_text,
'pos': pos,
}
synthetic = f'__SQL_CURSOR_{from_table}'
self.assignments.setdefault(synthetic, []).append(info)
@@ -1365,6 +1458,7 @@ class _BrParser:
'cursor_name': cursor_name,
'into_vars': into_vars,
'sql_text': sql_text,
'pos': pos,
}
for var in into_vars:
self.assignments.setdefault(var, []).append(info)
@@ -1396,6 +1490,7 @@ class _BrParser:
'where': where_clause,
'host_vars': host_vars,
'sql_text': sql_text,
'pos': pos,
}
synthetic = f'__SQL_UPDATE_{table}'
self.assignments.setdefault(synthetic, []).append(info)
@@ -1477,13 +1572,54 @@ def trace_to_root(field_name, assignments, fields, path_assign=None):
var = next_var
if next_var not in assignments:
break
elif len(sv) >= 2 and asgn.get('op') == '+':
# 多源加:取第一个源变量继续追溯
elif len(sv) >= 2 and asgn.get('op') in ('+', '-'):
# 多源加:取第一个源变量继续追溯
# 若其余源为常量字段(有 VALUE),折叠为单源 compute 供 invert 使用
folded = _fold_constants(asgn, fields)
if folded is not None:
chain[-1] = (var, folded)
var = sv[0]
else:
break
return var, chain
def _fold_constants(asgn, fields):
"""将 2 源 compute 折叠为单源 compute + const,供链式反演使用。
例如: T = A - CSTCST 为 WORKING-STORAGE 常量字段)
→ 折叠为: T = A op const,其中 op 反转为 +invert 时把 const 加回)。
返回新 asgn dict;若无法解析常量则返回 None(保持原 asgn)。
"""
if not asgn.get('source_vars') or len(asgn['source_vars']) != 2:
return None
sv = asgn['source_vars']
second = sv[1]
def _resolve(name):
for f in fields:
fname = f['name'] if isinstance(f, dict) else getattr(f, 'name', '')
if fname == name:
val = f.get('value', None) if isinstance(f, dict) else getattr(f, 'value', None)
if val is None:
val = (f.get('values') or [None])[0] if isinstance(f, dict) else \
getattr(f, 'values', None)
if isinstance(val, list):
val = val[0] if val else None
if val is not None:
try:
return float(str(val).strip("'\""))
except (ValueError, TypeError):
return None
return None
c = _resolve(second)
if c is None:
return None
new = dict(asgn)
new['source_vars'] = [sv[0]]
new['const'] = c
return new
def invert_through_chain(root_var, chain, operator, value):
op = operator
@@ -1964,10 +2100,10 @@ def classify_field_roles(tree, assignments, fields, source=None, proc_text=None)
# Phase 0: FD/OPEN 方向解析
fd_roles = {}
if source and proc_text:
from .read import parse_file_control, parse_file_section, scan_open_statements
from .read import parse_file_control, parse_file_section, scan_all_file_directions
file_ctl = parse_file_control(source)
file_sec = parse_file_section(source)
open_dir = scan_open_statements(proc_text)
open_dir = scan_all_file_directions(proc_text)
for iname, direction in open_dir.items():
if iname in file_sec:
for rec_name in file_sec[iname]:
@@ -2164,6 +2300,6 @@ def extract_sql_assignments(source: str) -> dict:
for m in _RE_EXEC_SQL.finditer(source):
sql_text = re.sub(r'\s+', ' ', m.group(1).strip())
parser._parse_sql(sql_text)
parser._parse_sql(sql_text, pos=m.start())
return dict(parser.assignments)