Skip to content

Commit 3e248ca

Browse files
Add local hook validation tooling scripts.
Provide hook index builders and validators used by the unified docs QA gate. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c8efcc6 commit 3e248ca

5 files changed

Lines changed: 1005 additions & 0 deletions

File tree

scripts/build_hook_index.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/usr/bin/env python3
2+
"""Build .reference/hook_index.json from PrestaShop 9.1.x sources."""
3+
4+
from __future__ import annotations
5+
6+
import json
7+
import sys
8+
from pathlib import Path
9+
10+
sys.path.insert(0, str(Path(__file__).resolve().parent))
11+
12+
from hook_utils import ( # noqa: E402
13+
HOOK_ALIAS_XML,
14+
HOOK_INDEX_PATH,
15+
HOOK_XML,
16+
HookMeta,
17+
find_occurrences,
18+
infer_hook_type,
19+
load_scope_files,
20+
parse_hook_alias_xml,
21+
parse_hook_xml,
22+
parse_title_from_front_matter,
23+
split_front_matter,
24+
)
25+
26+
27+
def hook_names_from_scope() -> list[str]:
28+
names: list[str] = []
29+
for path in load_scope_files():
30+
text = path.read_text(encoding="utf-8")
31+
fm, _ = split_front_matter(text)
32+
if not fm:
33+
continue
34+
title = parse_title_from_front_matter(fm)
35+
if title:
36+
names.append(title)
37+
return sorted(set(names))
38+
39+
40+
def build_index() -> dict[str, dict]:
41+
aliases_by_hook = parse_hook_alias_xml(HOOK_ALIAS_XML)
42+
hooks_xml = parse_hook_xml(HOOK_XML)
43+
index: dict[str, dict] = {}
44+
45+
for hook_name in hook_names_from_scope():
46+
xml_meta = hooks_xml.get(hook_name, {})
47+
occurrences = find_occurrences(hook_name)
48+
meta = HookMeta(
49+
name=hook_name,
50+
aliases=aliases_by_hook.get(hook_name, []),
51+
title=xml_meta.get("title", ""),
52+
description=xml_meta.get("description", ""),
53+
hook_type=infer_hook_type(hook_name),
54+
occurrences=occurrences,
55+
)
56+
index[hook_name] = {
57+
"aliases": meta.aliases,
58+
"title": meta.title,
59+
"description": meta.description,
60+
"type": meta.hook_type,
61+
"occurrences": [
62+
{
63+
"repo": occ.repo,
64+
"file": occ.file,
65+
"line": occ.line,
66+
"snippet": occ.snippet,
67+
}
68+
for occ in meta.occurrences
69+
],
70+
}
71+
72+
return index
73+
74+
75+
def main() -> int:
76+
if not HOOK_ALIAS_XML.exists():
77+
print(
78+
f"Missing {HOOK_ALIAS_XML}. Clone .reference/prestashop first.",
79+
file=sys.stderr,
80+
)
81+
return 1
82+
83+
index = build_index()
84+
HOOK_INDEX_PATH.parent.mkdir(parents=True, exist_ok=True)
85+
HOOK_INDEX_PATH.write_text(
86+
json.dumps(index, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
87+
)
88+
print(f"Wrote {HOOK_INDEX_PATH} ({len(index)} hooks)")
89+
with_occ = sum(1 for item in index.values() if item["occurrences"])
90+
print(f"Hooks with occurrences: {with_occ}")
91+
without = [name for name, item in index.items() if not item["occurrences"]]
92+
if without:
93+
print(f"Hooks without occurrences: {len(without)}")
94+
return 0
95+
96+
97+
if __name__ == "__main__":
98+
raise SystemExit(main())

scripts/fix_hook_pages.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env python3
2+
"""Apply hook metadata fixes for PR #2131 review feedback."""
3+
4+
from __future__ import annotations
5+
6+
import json
7+
import re
8+
import sys
9+
from pathlib import Path
10+
11+
sys.path.insert(0, str(Path(__file__).resolve().parent))
12+
13+
from hook_utils import ( # noqa: E402
14+
HOOK_INDEX_PATH,
15+
REPO_ROOT,
16+
Occurrence,
17+
fix_smarty_semicolons,
18+
format_files_yaml,
19+
format_hook_aliases_yaml,
20+
git_show_upstream,
21+
infer_hook_type,
22+
load_scope_files,
23+
parse_title_from_front_matter,
24+
parse_type_from_front_matter,
25+
replace_call_snippet,
26+
replace_files_block,
27+
replace_hook_aliases_block,
28+
replace_type,
29+
restore_protected_scalar_fields,
30+
split_front_matter,
31+
)
32+
33+
REMOVED_HOOKS = {"actionBeforeAjaxDie"}
34+
35+
36+
def load_index() -> dict:
37+
return json.loads(HOOK_INDEX_PATH.read_text(encoding="utf-8"))
38+
39+
40+
def occurrences_from_index(entry: dict) -> list[Occurrence]:
41+
seen_files: set[tuple[str, str]] = set()
42+
result: list[Occurrence] = []
43+
for item in entry.get("occurrences", []):
44+
key = (item["repo"], item["file"])
45+
if key in seen_files:
46+
continue
47+
seen_files.add(key)
48+
result.append(
49+
Occurrence(
50+
repo=item["repo"],
51+
file=item["file"],
52+
line=item["line"],
53+
snippet=item["snippet"],
54+
)
55+
)
56+
return result
57+
58+
59+
def fix_page(path: Path, index: dict) -> str | None:
60+
rel = path.relative_to(REPO_ROOT).as_posix()
61+
text = path.read_text(encoding="utf-8")
62+
fm, body = split_front_matter(text)
63+
if not fm:
64+
return None
65+
66+
hook_name = parse_title_from_front_matter(fm)
67+
if not hook_name:
68+
return None
69+
70+
entry = index.get(hook_name, {})
71+
if hook_name in REMOVED_HOOKS and not entry.get("occurrences"):
72+
return "DELETE"
73+
74+
upstream_text = git_show_upstream(rel) or ""
75+
upstream_fm, upstream_body = (
76+
split_front_matter(upstream_text) if upstream_text else ("", "")
77+
)
78+
79+
aliases = entry.get("aliases", [])
80+
occurrences = occurrences_from_index(entry)
81+
82+
updated_fm = fm
83+
updated_fm = replace_hook_aliases_block(
84+
updated_fm, format_hook_aliases_yaml(aliases, upstream_fm)
85+
)
86+
87+
upstream_type = parse_type_from_front_matter(upstream_fm or fm)
88+
hook_type = infer_hook_type(hook_name, upstream_type or None)
89+
if hook_type:
90+
updated_fm = replace_type(updated_fm, hook_type)
91+
92+
updated_fm = restore_protected_scalar_fields(updated_fm, upstream_fm, ["hookTitle"])
93+
updated_fm = replace_files_block(
94+
updated_fm, format_files_yaml(occurrences, upstream_fm)
95+
)
96+
97+
updated_body = body
98+
if occurrences:
99+
updated_body = replace_call_snippet(updated_body, occurrences[0].snippet)
100+
elif upstream_body:
101+
call_match = re.search(
102+
r"## Call of the Hook in the origin file\n\n```php\n(.*?)\n```",
103+
upstream_body,
104+
re.DOTALL,
105+
)
106+
if call_match:
107+
updated_body = replace_call_snippet(
108+
updated_body, call_match.group(1).strip()
109+
)
110+
111+
updated_body = fix_smarty_semicolons(updated_body)
112+
return updated_fm + updated_body
113+
114+
115+
def main() -> int:
116+
if not HOOK_INDEX_PATH.exists():
117+
print("Run build_hook_index.py first.", file=sys.stderr)
118+
return 1
119+
120+
index = load_index()
121+
changed = 0
122+
deleted = 0
123+
124+
for path in load_scope_files():
125+
if not path.exists():
126+
continue
127+
result = fix_page(path, index)
128+
if result == "DELETE":
129+
path.unlink()
130+
deleted += 1
131+
print(f"deleted {path.name}")
132+
continue
133+
if not result:
134+
continue
135+
original = path.read_text(encoding="utf-8")
136+
if result != original:
137+
path.write_text(result, encoding="utf-8")
138+
changed += 1
139+
140+
print(f"Updated {changed} files, deleted {deleted} files")
141+
return 0
142+
143+
144+
if __name__ == "__main__":
145+
raise SystemExit(main())

0 commit comments

Comments
 (0)