donau

Donation authority for GNU Taler (experimental)
Log | Files | Refs | Submodules | README | LICENSE

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)