fd2771d51dfa6526d51778a1a3e2e553aa75290c braney Tue Sep 1 10:37:16 2026 -0700 trackDbConditions: follow a setting to where its value is used, refs #37908 The first pass only saw a condition when it enclosed the read. That misses the commonest shape in the drawing code: the setting is read plainly at the top of a loader, carried into a struct, and used far below behind a test of a different setting. bamColorTag came back unconditional even though it does nothing unless bamColorMode is tag. So follow the value. A name that holds exactly one setting across a file is taken to carry it, including into a struct field of the same name, which is how the value usually travels. Then find where the value is used and intersect the conditions guarding those uses. Two distinctions do the work. A mention at paren depth zero is one side of an assignment or an element of an initializer list, which only moves the value somewhere else, so it is not a use; inside a call it is an argument and it is. Counting the struct initializer as a use put an unguarded site in the set and emptied every intersection. And matching drops the field prefix, so the test written sameString(colorMode, ...) in the loader is the same condition as sameString(btd->colorMode, ...) in the drawer. These are reported as "when used" and kept apart from "always". The every-path conditions are necessary by construction; a use-site condition is only as good as the set of uses found, so it is a strong hint rather than a claim, and it keeps the strict key rather than the loose one for that reason. 43 settings in the render scope are read plainly and used only under a condition, 21 of them documented. Among them: bamColorTag needs bamColorMode=tag, pairSearchRange needs pairEndsByName, speciesCodonDefault needs mafChain and frames, and speciesOrder, speciesGroups and speciesDefaultOff turn out to gate each other. diff --git src/hg/utils/trackDbConditions/trackDbConditions.py src/hg/utils/trackDbConditions/trackDbConditions.py index db778baecf7..d5691c18ab5 100755 --- src/hg/utils/trackDbConditions/trackDbConditions.py +++ src/hg/utils/trackDbConditions/trackDbConditions.py @@ -141,88 +141,104 @@ json.dump(reads, f) accessors = accessorMap(reads) settings = {} for read in reads: if not read["tdb"]: continue # a page cart variable, not a track setting key = (read["name"], read["scope"]) entry = settings.setdefault(key, {"name": read["name"], "scope": read["scope"], "sites": [], "unknown": False}) conds = [] for cond in hc.realConds(read): kind, named = classify(cond, accessors) conds.append({"text": cond["text"], "kind": kind, "names": named, "file": cond["file"], "line": cond["line"]}) + used = [] + for cond in read.get("useConds", []): + if hc.isNoise(cond): + continue + kind, named = classify(cond, accessors) + used.append({"text": cond["text"], "kind": kind, "names": named, + "file": cond["file"], "line": cond["line"]}) entry["sites"].append({"file": read["file"], "line": read["line"], "func": read["func"], "reader": read["reader"], - "conds": conds}) + "conds": conds, "used": used}) if read["callerUnknown"]: entry["unknown"] = True for entry in settings.values(): perSite = [{condId(c): c for c in s["conds"]} for s in entry["sites"]] always, sometimes = {}, {} if perSite: common = set(perSite[0]) for one in perSite[1:]: common &= set(one) for one in perSite: for cid, cond in one.items(): (always if cid in common else sometimes)[cid] = cond entry["always"] = sorted(always.values(), key=lambda c: (KIND_ORDER.index(c["kind"]), c["text"])) entry["sometimes"] = sorted(sometimes.values(), key=lambda c: (KIND_ORDER.index(c["kind"]), c["text"])) + perUse = [{condId(c): c for c in s["used"]} for s in entry["sites"] if s["used"]] + whenUsed = {} + if perUse: + common = set(perUse[0]) + for one in perUse[1:]: + common &= set(one) + whenUsed = {cid: c for one in perUse for cid, c in one.items() if cid in common} + entry["whenUsed"] = sorted(whenUsed.values(), + key=lambda c: (KIND_ORDER.index(c["kind"]), c["text"])) return list(settings.values()) def documented(kentSrc=None): """The settings trackDbLibrary.shtml describes, so the report can say which.""" kentSrc = kentSrc or os.environ.get("KENT_SRC") or "." path = os.path.join(kentSrc, SETTINGS_JSON) if not os.path.exists(path): return {} with open(path) as f: doc = json.load(f) return {s["key"]: s for s in doc["settings"]} def baselineNames(): if not os.path.exists(BASELINE): return set() with open(BASELINE) as f: return {ln.strip() for ln in f if ln.strip() and not ln.startswith("#")} def conditionedNames(entries, docs, scope="render"): """Documented settings that carry an always-condition, as scope:name.""" return {"%s:%s" % (e["scope"], e["name"]) for e in entries - if e["scope"] == scope and e["always"] and e["name"] in docs} + if e["scope"] == scope and (e["always"] or e["whenUsed"]) and e["name"] in docs} def showSetting(entry, docs, verbose=False): doc = docs.get(entry["name"]) types = doc["types"] if doc else None if isinstance(types, list): types = ", ".join(types) print("%s [%s]" % (entry["name"], entry["scope"])) if doc: print(" documented for: %s" % (types or "?")) else: print(" not in trackDbLibrary.shtml") - groups = [("always", entry["always"])] + groups = [("always", entry["always"]), ("when used", entry["whenUsed"])] if verbose: groups.append(("sometimes", entry["sometimes"])) elif entry["sometimes"]: print(" (%d more conditions hold at some of its %d read sites; --verbose to see)" % (len(entry["sometimes"]), len(entry["sites"]))) for label, conds in groups: for cond in conds: names = (" -> " + ", ".join(cond["names"])) if cond["names"] else "" print(" %-9s %-12s %s%s" % (label, cond["kind"], cond["text"][:88], names)) if entry["unknown"]: print(" (some call paths could not be followed, so this is a floor)") if verbose: for site in entry["sites"]: print(" %s:%d %s()" % (site["file"], site["line"], site["func"])) @@ -281,48 +297,51 @@ problems += 1 sys.exit(1 if problems else 0) scopes = list(hc.SCOPES) if args.scope == "both" else [args.scope] picked = [e for e in entries if e["scope"] in scopes] if args.documented: picked = [e for e in picked if e["name"] in docs] if args.setting: picked = [e for e in picked if e["name"] == args.setting] for entry in picked: showSetting(entry, docs, verbose=True) if not picked: print("no read of %s found in %s" % (args.setting, ", ".join(scopes))) return - conditioned = [e for e in picked if e["always"] or e["sometimes"]] + conditioned = [e for e in picked if e["always"] or e["sometimes"] or e["whenUsed"]] if args.kind: conditioned = [e for e in conditioned if any(c["kind"] == args.kind for c in e["always"] + e["sometimes"])] if args.surprising: conditioned = [e for e in conditioned - if any(c["kind"] in SURPRISING for c in e["always"])] + if any(c["kind"] in SURPRISING for c in e["always"] + e["whenUsed"])] if args.list or args.kind or args.surprising: for entry in sorted(conditioned, key=lambda e: (e["scope"], e["name"].lower())): showSetting(entry, docs, args.verbose) print() inDocs = [e for e in conditioned if e["name"] in docs] always = [e for e in conditioned if e["always"]] + used = [e for e in conditioned if e["whenUsed"]] print("settings read %d (%s)" % (len(picked), ", ".join(scopes))) print("with any condition %d" % len(conditioned)) print("with an always-condition %d" % len(always)) print("of those, documented %d" % len([e for e in always if e["name"] in docs])) + print("read plainly, used only under a condition %d (documented %d)" + % (len(used), len([e for e in used if e["name"] in docs]))) print("documented and conditional at all: %d" % len(inDocs)) print() tally = collections.Counter() for entry in conditioned: - for cond in entry["always"]: + for cond in entry["always"] + entry["whenUsed"]: tally[cond["kind"]] += 1 print("always-conditions by kind:") for kind in KIND_ORDER: if tally[kind]: print(" %-14s %3d %s" % (kind, tally[kind], KIND_TEXT[kind])) if __name__ == "__main__": main()