feat: SQL between/hostvar-key alignment, class-condition parsing, gcov merge across scenario runs
This commit is contained in:
+147
-11
@@ -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/BEFORE(UNTIL 在下一行)
|
||||
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 - CST(CST 为 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)
|
||||
|
||||
Reference in New Issue
Block a user