Skip to content

Commit 14d3f22

Browse files
committed
Update opm-common submodule and rebuild keyword index
Advance the opm-common submodule pointer and regenerate the keyword index from it. Also tolerate the non-strict JSON a few opm-common keyword files use: multi-line comment strings with raw newlines (ROCK, MAPAXES), number literals like 1.e-01 (NETBALAN), and an empty placeholder (REACACT). A sanitizing fallback loads these instead of dropping them with a warning, recovering 3 keywords in the index.
1 parent e670308 commit 14d3f22

4 files changed

Lines changed: 136 additions & 4 deletions

File tree

opm-common

Submodule opm-common updated 103 files

scripts/build_keyword_index.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,66 @@ def _classify_size(opm_data: dict) -> tuple[str, Optional[int]]:
157157
return "none", 0
158158

159159

160+
def _sanitize_lax_json(text: str) -> str:
161+
"""Rewrite the non-strict JSON a handful of opm-common keyword files use
162+
into something Python's strict ``json`` accepts. opm-common ships these
163+
through its own lenient C++ parser, so two constructs slip in:
164+
165+
* multi-line ``"comment"`` strings containing raw newlines/tabs (e.g.
166+
ROCK, MAPAXES) — control chars are illegal inside JSON strings;
167+
* number literals with no digit after the decimal point, like
168+
``1.e-01`` in NETBALAN — JSON requires ``1.0e-01``.
169+
170+
A single left-to-right scan tracks whether we're inside a string so the
171+
two fixes never interfere: control chars are escaped only inside strings,
172+
and the ``.`` -> ``.0`` fix is applied only outside them (in JSON a bare
173+
``.`` can only occur within a number).
174+
"""
175+
out: list[str] = []
176+
in_string = False
177+
escaped = False
178+
ctrl_map = {"\n": "\\n", "\r": "\\r", "\t": "\\t"}
179+
n = len(text)
180+
for i, ch in enumerate(text):
181+
if in_string:
182+
if escaped:
183+
out.append(ch)
184+
escaped = False
185+
elif ch == "\\":
186+
out.append(ch)
187+
escaped = True
188+
elif ch == '"':
189+
out.append(ch)
190+
in_string = False
191+
elif ord(ch) < 0x20:
192+
out.append(ctrl_map.get(ch, "\\u%04x" % ord(ch)))
193+
else:
194+
out.append(ch)
195+
elif ch == '"':
196+
in_string = True
197+
out.append(ch)
198+
elif ch == "." and i + 1 < n and text[i + 1] in "eE":
199+
# 1.e-01 -> 1.0e-01
200+
out.append(".0")
201+
else:
202+
out.append(ch)
203+
return "".join(out)
204+
205+
206+
def _load_keyword_json(kw_file: Path) -> Optional[dict]:
207+
"""Read one opm-common keyword file, tolerating the lenient JSON a few
208+
upstream files use. Returns the parsed dict, ``None`` for an empty file
209+
(e.g. REACACT, a placeholder), and re-raises only if even the sanitized
210+
text fails to parse."""
211+
text = kw_file.read_text(encoding="utf-8")
212+
if not text.strip():
213+
return None
214+
try:
215+
return json.loads(text)
216+
except json.JSONDecodeError:
217+
return json.loads(_sanitize_lax_json(text))
218+
219+
160220
def load_opm_common_index(keywords_dir: Path) -> dict:
161221
"""
162222
Walk the opm-common keywords tree and return a dict keyed by keyword name.
@@ -179,11 +239,12 @@ def load_opm_common_index(keywords_dir: Path) -> dict:
179239
if not kw_file.is_file():
180240
continue
181241
try:
182-
with open(kw_file, "r", encoding="utf-8") as f:
183-
data = json.load(f)
242+
data = _load_keyword_json(kw_file)
184243
except (OSError, json.JSONDecodeError) as e:
185244
print(f" WARNING: failed to read {kw_file}: {e}", file=sys.stderr)
186245
continue
246+
if data is None: # empty placeholder file
247+
continue
187248
name = data.get("name") or kw_file.name
188249
if name in out:
189250
continue

scripts/test_build_keyword_index.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
attach_string_options,
4242
_opm_item_for_param,
4343
_classify_size,
44+
_sanitize_lax_json,
45+
_load_keyword_json,
4446
_summary_size_shape,
4547
_summary_optional_body,
4648
NS,
@@ -685,6 +687,75 @@ def test_title_size_kind_overridden_to_none(self, tmp_path):
685687
assert idx["TITLE"]["size_kind"] == "none"
686688
assert idx["TITLE"]["size_count"] is None
687689

690+
def test_multiline_comment_is_loaded(self, tmp_path):
691+
# ROCK / MAPAXES ship a "comment" with raw newlines, which strict
692+
# JSON rejects. The lenient loader should still pick the keyword up.
693+
d = tmp_path / "000_Eclipse100" / "R"
694+
d.mkdir(parents=True)
695+
(d / "ROCK").write_text(
696+
'{\n "name": "ROCK",\n "sections": ["PROPS"],\n'
697+
' "comment" : "\nline one\nline two\n",\n "items": []\n}\n',
698+
encoding="utf-8",
699+
)
700+
idx = load_opm_common_index(tmp_path)
701+
assert "ROCK" in idx
702+
assert idx["ROCK"]["sections"] == ["PROPS"]
703+
704+
def test_lax_number_literal_is_loaded(self, tmp_path):
705+
# NETBALAN uses 1.e-01, which JSON requires written as 1.0e-01.
706+
d = tmp_path / "000_Eclipse100" / "N"
707+
d.mkdir(parents=True)
708+
(d / "NETBALAN").write_text(
709+
'{ "name": "NETBALAN", "sections": ["SCHEDULE"],'
710+
' "items": [{ "name": "LIMIT", "value_type": "DOUBLE",'
711+
' "default": 1.e-01 }] }',
712+
encoding="utf-8",
713+
)
714+
idx = load_opm_common_index(tmp_path)
715+
assert "NETBALAN" in idx
716+
assert idx["NETBALAN"]["items"][0]["default"] == 0.1
717+
718+
def test_empty_file_is_skipped_without_error(self, tmp_path):
719+
# REACACT is an empty placeholder file; it should drop out quietly.
720+
d = tmp_path / "000_Eclipse100" / "R"
721+
d.mkdir(parents=True)
722+
(d / "REACACT").write_text("", encoding="utf-8")
723+
idx = load_opm_common_index(tmp_path)
724+
assert idx == {}
725+
726+
727+
class TestSanitizeLaxJson:
728+
def test_escapes_control_chars_inside_strings(self):
729+
out = _sanitize_lax_json('{ "c": "a\nb\tc" }')
730+
assert json.loads(out)["c"] == "a\nb\tc"
731+
732+
def test_fixes_decimal_without_trailing_digit(self):
733+
assert json.loads(_sanitize_lax_json('{ "v": 1.e-01 }'))["v"] == 0.1
734+
735+
def test_leaves_dot_inside_strings_alone(self):
736+
# A literal ".e" sequence inside a string must not be rewritten.
737+
out = _sanitize_lax_json('{ "c": "see 1.e-01 here", "v": 2.e0 }')
738+
data = json.loads(out)
739+
assert data["c"] == "see 1.e-01 here"
740+
assert data["v"] == 2.0
741+
742+
def test_well_formed_json_is_unchanged(self):
743+
src = '{ "a": 1, "b": [1.5, 2.0], "c": "x" }'
744+
assert json.loads(_sanitize_lax_json(src)) == json.loads(src)
745+
746+
747+
class TestLoadKeywordJson:
748+
def test_empty_file_returns_none(self, tmp_path):
749+
p = tmp_path / "EMPTY"
750+
p.write_text(" \n", encoding="utf-8")
751+
assert _load_keyword_json(p) is None
752+
753+
def test_truly_broken_json_still_raises(self, tmp_path):
754+
p = tmp_path / "BAD"
755+
p.write_text("{ not valid", encoding="utf-8")
756+
with pytest.raises(json.JSONDecodeError):
757+
_load_keyword_json(p)
758+
688759

689760
class TestClassifySize:
690761
def test_explicit_size_zero_means_none(self):

vscode-extension/data/keyword_index_compact.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)