check-db-naming.py (5221B)
1 #!/usr/bin/env python3 2 """Enforce the database layer naming convention. 3 4 Rules checked (see the commit series "database naming cleanup"): 5 6 1. every public function's name starts with the name of the file it lives in 7 2. the file's own name uses a prefix from the closed set, or is exempt 8 3. iterate_ iff the function takes a caller-supplied callback; get_ iff not 9 4. a .h exists beside every .c, and a .sql (if any) shares the name 10 5. every prepared statement is named after the function preparing it 11 6. no prepared statement name is used in two files 12 7. no numeric suffixes and no repeated library prefix 13 14 Run from the top of the source tree; exits non-zero on any violation. 15 """ 16 import re 17 import sys 18 import pathlib 19 20 # ---------------------------------------------------------------- CONFIG -- 21 # (impl dir, header dir, symbol prefix, .c basename prefix) 22 LAYERS = [ 23 ("src/donaudb", "src/include/donau-database", "DONAUDB_", ""), 24 ] 25 26 # Prefixes that describe what the caller gets. 27 PREFIXES = ("get_", "get_count_", "get_exists_", "iterate_", 28 "insert_", "update_", "update_to_", "delete_", "do_") 29 30 # Not CRUD: transaction control, DDL, events, memory, session plumbing, 31 # cancellable handle APIs, and multi-function helper modules. Keep this list 32 # short and justified -- every entry is a rule the checker stops enforcing. 33 EXEMPT = { 34 "start", "start_read_only", "start_read_committed", "commit", "rollback", 35 "preflight", "create_tables", "drop_tables", 36 "event_listen", "event_listen_cancel", "event_notify", 37 } 38 39 # Files that are not part of the API surface at all. plugin_donaudb_postgres.c 40 # is not a plugin vtable despite the name; it holds the connect/disconnect and 41 # reconnect plumbing, and is skipped by the plugin_ rule below. 42 SKIP = {"helper", "pg_template"} 43 # ------------------------------------------------------------ END CONFIG -- 44 45 PREP = re.compile(r'PREPARE\s*\(\s*\w+\s*,\s*"([A-Za-z0-9_]+)"', re.S) 46 47 errors = [] 48 49 50 def check_layer(impl_dir, hdr_dir, prefix, fpfx): 51 impl, hdr = pathlib.Path(impl_dir), pathlib.Path(hdr_dir) 52 if not impl.is_dir(): 53 return 54 decl = re.compile(r"^" + re.escape(prefix) + r"(\w+)\s*\((.*?)\)\s*;", 55 re.S | re.M) 56 stmt_owner = {} 57 58 for c in sorted(impl.glob("*.c")): 59 if c.stem in SKIP or c.stem.startswith("test_") \ 60 or c.stem.startswith("plugin_"): 61 continue 62 if fpfx and not c.stem.startswith(fpfx): 63 continue 64 stem = c.stem[len(fpfx):] if fpfx else c.stem 65 66 # 2. prefix from the closed set, or exempt 67 if stem not in EXEMPT and not stem.startswith(PREFIXES): 68 errors.append(f"{c}: '{stem}' uses no approved prefix") 69 70 # 7. numeric suffix 71 if re.search(r"\d$", stem): 72 errors.append(f"{c}: '{stem}' ends in a digit") 73 74 # 4. matching header 75 h = hdr / (stem + ".h") 76 if stem not in EXEMPT and not h.exists(): 77 errors.append(f"{c}: no matching header {h}") 78 79 text = c.read_text(errors="replace") 80 81 # 5. prepared statements named after the function 82 for s in dict.fromkeys(PREP.findall(text)): 83 if s != stem and not s.startswith(stem + "_"): 84 errors.append( 85 f"{c}: prepared statement '{s}' is not named after " 86 f"'{stem}'") 87 # 6. cross-file uniqueness 88 if s in stmt_owner and stmt_owner[s] != str(c): 89 errors.append( 90 f"{c}: prepared statement '{s}' also prepared in " 91 f"{stmt_owner[s]}") 92 stmt_owner[s] = str(c) 93 94 if not h.exists(): 95 continue 96 htext = re.sub(r"/\*.*?\*/", "", h.read_text(errors="replace"), 97 flags=re.S) 98 for name, params in decl.findall(htext): 99 # 1. declared names belong to their file. Exempt entries are the 100 # documented multi-function modules, so this does not apply there. 101 if (stem not in EXEMPT 102 and name != stem and not name.startswith(stem + "_")): 103 errors.append( 104 f"{h}: declares '{name}', which does not belong to " 105 f"'{stem}'") 106 # 3. iterate_ iff callback 107 has_cb = bool(re.search(r"\w*(Callback|Iterator)\s+\w+", params)) 108 if name.startswith("iterate_") and not has_cb: 109 errors.append( 110 f"{h}: '{name}' is iterate_ but takes no callback") 111 if name.startswith("get_") and has_cb: 112 errors.append(f"{h}: '{name}' takes a callback; use iterate_") 113 114 # 7. repeated library prefix anywhere in the tree 115 head = prefix.split("_")[0] 116 for f in list(impl.glob("*.[ch]")) + list(hdr.glob("*.h")): 117 if "unc-backup" in f.name: 118 continue 119 if re.search(r"\b" + re.escape(head) + r"_" + re.escape(prefix), 120 f.read_text(errors="replace")): 121 errors.append(f"{f}: repeated '{prefix}' prefix") 122 123 124 for a in LAYERS: 125 check_layer(*a) 126 127 for e in errors: 128 print("ERROR:", e) 129 print(f"\ndb-naming: {len(errors)} violation(s)") 130 sys.exit(1 if errors else 0)