Notepad3/Build/scripts/check_scicall.py

387 lines
13 KiB
Python

#!/usr/bin/env python3
"""
check_scicall.py - Diff Scintilla.iface against SciCall.h
Parses Scintilla.iface to find all fun/get/set messages and compares
against SciCall.h to find:
1. iface messages NOT wrapped in SciCall.h (candidates to add)
2. SciCall.h wrappers that don't match any iface message (stale/custom)
Usage:
python Build/scripts/check_scicall.py
python Build/scripts/check_scicall.py --verbose
python Build/scripts/check_scicall.py --category Basics
python Build/scripts/check_scicall.py --generate
"""
import argparse
import os
import re
import sys
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.normpath(os.path.join(SCRIPT_DIR, "..", ".."))
IFACE_PATH = os.path.join(REPO_ROOT, "scintilla", "include", "Scintilla.iface")
SCICALL_PATH = os.path.join(REPO_ROOT, "src", "SciCall.h")
# iface type -> NP3 C type mapping
TYPE_MAP = {
"void": "void",
"position": "DocPos",
"line": "DocLn",
"colour": "COLORREF",
"colouralpha": "COLORALPHAREF",
"bool": "bool",
"int": "int",
"string": "const char*",
"stringresult": "char*",
"pointer": "sptr_t",
"cells": "const char*",
"textrange": "struct Sci_TextRange*",
"textrangefull": "struct Sci_TextRangeFull*",
"findtext": "struct Sci_TextToFind*",
"findtextfull": "struct Sci_TextToFindFull*",
"formatrange": "struct Sci_RangeToFormat*",
"formatrangefull": "struct Sci_RangeToFormatFull*",
"keymod": "size_t",
}
def map_type(iface_type):
"""Map an iface type to an NP3 C type."""
if iface_type in TYPE_MAP:
return TYPE_MAP[iface_type]
# Capitalized enum types -> int
if iface_type and iface_type[0].isupper():
return "int"
return "int"
def parse_iface(path):
"""Parse Scintilla.iface, returning a dict of uppercase_name -> info."""
messages = {}
current_category = "Unknown"
# Match: fun/get/set <rettype> <Name>=<number>(<params>)
msg_re = re.compile(
r"^(fun|get|set)\s+" # feature type
r"(\S+)\s+" # return type
r"(\w+)" # function name
r"=(\d+)" # message number
r"\(([^)]*)\)" # parameters
)
cat_re = re.compile(r"^cat\s+(\w+)")
with open(path, "r", encoding="utf-8") as f:
for line_no, line in enumerate(f, 1):
line = line.rstrip()
cat_m = cat_re.match(line)
if cat_m:
current_category = cat_m.group(1)
continue
if line.startswith("##") or line.startswith("#!"):
continue
msg_m = msg_re.match(line)
if msg_m:
feat_type = msg_m.group(1)
ret_type = msg_m.group(2)
name = msg_m.group(3)
msg_num = int(msg_m.group(4))
params_str = msg_m.group(5).strip()
upper_name = name.upper()
messages[upper_name] = {
"name": name,
"upper": upper_name,
"type": feat_type,
"ret": ret_type,
"num": msg_num,
"params": params_str,
"category": current_category,
"line": line_no,
"raw": line,
}
return messages
def parse_scicall(path):
"""Parse SciCall.h, returning a dict of uppercase_msg -> info."""
wrappers = {}
# Match: DeclareSciCall{R,V}{0,01,1,2}(fn, MSG, ...)
decl_re = re.compile(
r"DeclareSciCall([RV])(0|01|1|2)\s*\(\s*(\w+)\s*,\s*(\w+)"
)
# Also match commented-out declarations
commented_re = re.compile(
r"//~?\s*DeclareSciCall([RV])(0|01|1|2)\s*\(\s*(\w+)\s*,\s*(\w+)"
)
with open(path, "r", encoding="utf-8") as f:
for line_no, line in enumerate(f, 1):
line_stripped = line.rstrip()
# Skip macro definitions (#define DeclareSciCall...)
if line_stripped.startswith("#define"):
continue
# Check for commented-out declarations (track but mark)
cm = commented_re.match(line_stripped)
if cm:
upper_msg = cm.group(4)
wrappers[upper_msg] = {
"fn": cm.group(3),
"msg": upper_msg,
"macro_ret": cm.group(1),
"macro_params": cm.group(2),
"line": line_no,
"commented": True,
"raw": line_stripped,
}
continue
dm = decl_re.search(line_stripped)
if dm:
upper_msg = dm.group(4)
wrappers[upper_msg] = {
"fn": dm.group(3),
"msg": upper_msg,
"macro_ret": dm.group(1),
"macro_params": dm.group(2),
"line": line_no,
"commented": False,
"raw": line_stripped,
}
return wrappers
def parse_param(param_str):
"""Parse a single iface param like 'position pos' or '' into (type, name)."""
param_str = param_str.strip()
if not param_str:
return None, None
# Handle default values like 'int defaultValue'
parts = param_str.split()
if len(parts) >= 2:
return parts[0], parts[1].split("=")[0]
elif len(parts) == 1:
return parts[0], "param"
return None, None
def generate_wrapper(msg):
"""Generate a DeclareSciCall* line for an iface message."""
name = msg["name"]
upper = msg["upper"]
ret_type = msg["ret"]
params_str = msg["params"]
# Parse return type
c_ret = map_type(ret_type)
is_void = (ret_type == "void")
# Parse parameters
if "," in params_str:
wp_str, lp_str = params_str.split(",", 1)
else:
wp_str = params_str
lp_str = ""
wp_type, wp_name = parse_param(wp_str)
lp_type, lp_name = parse_param(lp_str)
has_wp = wp_type is not None
has_lp = lp_type is not None
# Determine macro variant
rv = "V" if is_void else "R"
if has_wp and has_lp:
variant = "2"
elif has_wp and not has_lp:
variant = "1"
elif not has_wp and has_lp:
variant = "01"
else:
variant = "0"
macro = f"DeclareSciCall{rv}{variant}"
# Build arguments
if variant == "0":
if is_void:
return f"{macro}({name}, {upper});"
else:
return f"{macro}({name}, {upper}, {c_ret});"
elif variant == "1":
c_wp = map_type(wp_type)
if is_void:
return f"{macro}({name}, {upper}, {c_wp}, {wp_name});"
else:
return f"{macro}({name}, {upper}, {c_ret}, {c_wp}, {wp_name});"
elif variant == "01":
c_lp = map_type(lp_type)
if is_void:
return f"{macro}({name}, {upper}, {c_lp}, {lp_name});"
else:
return f"{macro}({name}, {upper}, {c_ret}, {c_lp}, {lp_name});"
elif variant == "2":
c_wp = map_type(wp_type)
c_lp = map_type(lp_type)
if is_void:
return f"{macro}({name}, {upper}, {c_wp}, {wp_name}, {c_lp}, {lp_name});"
else:
return f"{macro}({name}, {upper}, {c_ret}, {c_wp}, {wp_name}, {c_lp}, {lp_name});"
return f"// TODO: {name}"
def main():
parser = argparse.ArgumentParser(
description="Diff Scintilla.iface against SciCall.h to find unwrapped messages"
)
parser.add_argument(
"--verbose", "-v", action="store_true",
help="Show detailed info for each unwrapped message"
)
parser.add_argument(
"--category", "-c", type=str, default=None,
help="Filter to a specific iface category (e.g. Basics, Provisional, Deprecated)"
)
parser.add_argument(
"--show-wrapped", "-w", action="store_true",
help="Also list messages that ARE wrapped (for completeness check)"
)
parser.add_argument(
"--generate", "-g", action="store_true",
help="Generate DeclareSciCall* lines for all unwrapped non-deprecated messages"
)
parser.add_argument(
"--iface", type=str, default=IFACE_PATH,
help=f"Path to Scintilla.iface (default: {IFACE_PATH})"
)
parser.add_argument(
"--scicall", type=str, default=SCICALL_PATH,
help=f"Path to SciCall.h (default: {SCICALL_PATH})"
)
args = parser.parse_args()
if not os.path.isfile(args.iface):
print(f"Error: {args.iface} not found", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(args.scicall):
print(f"Error: {args.scicall} not found", file=sys.stderr)
sys.exit(1)
all_iface_msgs = parse_iface(args.iface)
scicall_wrappers = parse_scicall(args.scicall)
# Apply category filter for unwrapped/wrapped analysis
if args.category:
iface_msgs = {
k: v for k, v in all_iface_msgs.items()
if v["category"].lower() == args.category.lower()
}
else:
iface_msgs = all_iface_msgs
iface_keys = set(iface_msgs.keys())
all_iface_keys = set(all_iface_msgs.keys())
scicall_keys = set(scicall_wrappers.keys())
unwrapped = sorted(iface_keys - scicall_keys, key=lambda k: iface_msgs[k]["num"])
# Stale check always uses the full iface (not filtered)
stale = sorted(scicall_keys - all_iface_keys)
wrapped = sorted(iface_keys & scicall_keys, key=lambda k: iface_msgs[k]["num"])
# Generate mode: output DeclareSciCall* lines grouped by category
if args.generate:
non_deprecated = [
k for k in unwrapped if iface_msgs[k]["category"] != "Deprecated"
]
by_cat = {}
for key in non_deprecated:
cat = iface_msgs[key]["category"]
by_cat.setdefault(cat, []).append(key)
for cat in sorted(by_cat.keys()):
keys = by_cat[cat]
print(f"// --- [{cat}] ({len(keys)} wrappers) ---")
for key in keys:
msg = iface_msgs[key]
line = generate_wrapper(msg)
print(line)
print()
print(f"// Total: {len(non_deprecated)} generated wrappers")
return 0
# Group unwrapped by category
unwrapped_by_cat = {}
for key in unwrapped:
cat = iface_msgs[key]["category"]
unwrapped_by_cat.setdefault(cat, []).append(key)
# Summary
cat_label = f" (category: {args.category})" if args.category else ""
print(f"=== SciCall.h Coverage Report{cat_label} ===")
print(f" iface messages (fun/get/set): {len(iface_msgs)}")
print(f" SciCall.h wrappers: {len(scicall_wrappers)}")
print(f" Wrapped (matched): {len(wrapped)}")
print(f" Unwrapped (missing): {len(unwrapped)}")
print(f" Stale/custom (no iface): {len(stale)}")
print()
# Unwrapped messages
if unwrapped:
print(f"--- Unwrapped iface messages ({len(unwrapped)}) ---")
for cat in sorted(unwrapped_by_cat.keys()):
keys = unwrapped_by_cat[cat]
print(f"\n [{cat}] ({len(keys)} messages)")
for key in keys:
msg = iface_msgs[key]
if args.verbose:
print(f" SCI_{key} = {msg['num']}")
print(f" {msg['type']} {msg['ret']} {msg['name']}({msg['params']})")
print(f" iface line {msg['line']}")
else:
print(f" SCI_{key:<45s} {msg['type']:<4s} {msg['ret']:<16s} {msg['name']}({msg['params']})")
print()
# Stale wrappers (in SciCall.h but not in iface)
if stale:
print(f"--- Stale/custom wrappers ({len(stale)}) ---")
print(" (In SciCall.h but no matching iface fun/get/set)")
for key in stale:
w = scicall_wrappers[key]
status = " [commented]" if w["commented"] else ""
print(f" SCI_{key:<45s} SciCall_{w['fn']:<30s} line {w['line']}{status}")
print()
# Wrapped messages (optional)
if args.show_wrapped and wrapped:
print(f"--- Wrapped messages ({len(wrapped)}) ---")
for key in wrapped:
msg = iface_msgs[key]
w = scicall_wrappers[key]
status = " [commented]" if w["commented"] else ""
print(f" SCI_{key:<45s} -> SciCall_{w['fn']}{status}")
print()
# Exit code: 0 if no unwrapped non-deprecated messages, 1 otherwise
non_deprecated_unwrapped = [
k for k in unwrapped if iface_msgs[k]["category"] != "Deprecated"
]
if non_deprecated_unwrapped:
print(f"({len(non_deprecated_unwrapped)} unwrapped non-deprecated messages)")
return 0
if __name__ == "__main__":
sys.exit(main())