exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

check-db-naming.py (5322B)


      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 import collections
     20 
     21 LAYERS = [
     22     ("src/exchangedb", "src/include/exchange-database", "TALER_EXCHANGEDB_"),
     23     ("src/auditordb", "src/include/auditor-database", "TALER_AUDITORDB_"),
     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, shard lifecycle, and multi-function helper modules.
     32 EXEMPT = {
     33     "start", "start_read_only", "start_read_committed",
     34     "start_deferred_wire_out", "commit", "rollback", "preflight", "disconnect",
     35     "create_tables", "drop_tables", "inject_auditor_triggers", "gc",
     36     "event_listen", "event_listen_cancel", "event_notify",
     37     "free_coin_transaction_list", "free_reserve_history",
     38     "compute_shard", "enable_rules", "disable_rules", "begin_rule_update",
     39     "begin_shard", "begin_revolving_shard", "complete_shard", "abort_shard",
     40     "release_revolving_shard",
     41     "account_history", "exchangedb_accounts", "exchangedb_transactions",
     42 }
     43 
     44 # Transaction functions intentionally share this implementation file.
     45 STATEMENT_OWNERS = {
     46     "src/auditordb/start.c": ("start", "commit", "rollback"),
     47 }
     48 
     49 # Files that are not part of the API surface at all.
     50 SKIP = {"pg", "template", "helper", "bench_db"}
     51 
     52 PREP = re.compile(r'PREPARE\s*\(\s*\w+\s*,\s*"([A-Za-z0-9_]+)"', re.S)
     53 
     54 errors = []
     55 
     56 
     57 def check_layer(impl_dir, hdr_dir, prefix):
     58     impl, hdr = pathlib.Path(impl_dir), pathlib.Path(hdr_dir)
     59     if not impl.is_dir():
     60         return
     61     decl = re.compile(r"^" + re.escape(prefix) + r"(\w+)\s*\((.*?)\)\s*;",
     62                       re.S | re.M)
     63     stmt_owner = {}
     64 
     65     for c in sorted(impl.glob("*.c")):
     66         stem = c.stem
     67         if stem in SKIP or stem.startswith("test_"):
     68             continue
     69 
     70         # 2. prefix from the closed set, or exempt
     71         if stem not in EXEMPT and not stem.startswith(PREFIXES):
     72             errors.append(f"{c}: '{stem}' uses no approved prefix")
     73 
     74         # 7. numeric suffix
     75         if re.search(r"\d$", stem):
     76             errors.append(f"{c}: '{stem}' ends in a digit")
     77 
     78         # 4. matching header
     79         h = hdr / (stem + ".h")
     80         if stem not in EXEMPT and not h.exists():
     81             errors.append(f"{c}: no matching header {h}")
     82 
     83         text = c.read_text(errors="replace")
     84 
     85         # 5. prepared statements named after the function
     86         owners = STATEMENT_OWNERS.get(c.as_posix(), (stem,))
     87         for s in dict.fromkeys(PREP.findall(text)):
     88             if not any(s == owner or s.startswith(owner + "_")
     89                        for owner in owners):
     90                 errors.append(
     91                     f"{c}: prepared statement '{s}' is not named after '{stem}'")
     92             # 6. cross-file uniqueness
     93             if s in stmt_owner and stmt_owner[s] != str(c):
     94                 errors.append(
     95                     f"{c}: prepared statement '{s}' also prepared in "
     96                     f"{stmt_owner[s]}")
     97             stmt_owner[s] = str(c)
     98 
     99         if not h.exists():
    100             continue
    101         htext = re.sub(r"/\*.*?\*/", "", h.read_text(errors="replace"),
    102                        flags=re.S)
    103         for name, params in decl.findall(htext):
    104             # 1. declared names belong to their file.  Exempt entries are the
    105             # documented multi-function modules, so this does not apply there.
    106             if (stem not in EXEMPT
    107                     and name != stem and not name.startswith(stem + "_")):
    108                 errors.append(
    109                     f"{h}: declares '{name}', which does not belong to "
    110                     f"'{stem}'")
    111             # 3. iterate_ iff callback
    112             has_cb = bool(re.search(r"\w*(Callback|Iterator)\s+\w+", params))
    113             if name.startswith("iterate_") and not has_cb:
    114                 errors.append(f"{h}: '{name}' is iterate_ but takes no callback")
    115             if name.startswith("get_") and has_cb:
    116                 errors.append(f"{h}: '{name}' takes a callback; use iterate_")
    117 
    118     # 7. repeated library prefix anywhere in the tree
    119     for f in list(impl.glob("*.[ch]")) + list(hdr.glob("*.h")):
    120         if "unc-backup" in f.name:
    121             continue
    122         if re.search(r"\b" + re.escape(prefix.split("_")[0]) + r"_" +
    123                      re.escape(prefix), f.read_text(errors="replace")):
    124             errors.append(f"{f}: repeated '{prefix}' prefix")
    125 
    126 
    127 for a in LAYERS:
    128     check_layer(*a)
    129 
    130 for e in errors:
    131     print("ERROR:", e)
    132 print(f"\ndb-naming: {len(errors)} violation(s)")
    133 sys.exit(1 if errors else 0)