diff --git a/.github/workflows/metrics.yml b/.github/workflows/metrics.yml new file mode 100644 index 00000000000..4ec3e2ac91a --- /dev/null +++ b/.github/workflows/metrics.yml @@ -0,0 +1,41 @@ +name: Metrics + +on: [pull_request, workflow_dispatch] + +jobs: + customization: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 # need the base commit to compare against + + - name: Setup python + uses: actions/setup-python@v6 + with: + python-version: '3.14' + + - name: Add base commit worktree + if: github.event_name == 'pull_request' + run: | + # HEAD is the PR merge commit, so its first parent is the base branch + # tip and the delta covers only this PR's own changes. Not + # pull_request.base.sha: that is the fork point, so it drifts behind as + # the base branch advances and the delta then also reports commits from + # everyone else that got merged in. + if git rev-parse --verify -q HEAD^2 >/dev/null; then + git worktree add ../base "$(git rev-parse HEAD^1)" + else + echo "::warning::HEAD is not a merge commit; deltas will be omitted" + fi + + - name: Report customization + run: | + # The base tree is measured with *this* PR's script, so the delta + # reflects the code change rather than a change to how we measure. + if [ -d ../base ]; then + set -- --base ../base/lua/wikis + fi + python3 scripts/customization_metrics.py "$@" | tee -a "$GITHUB_STEP_SUMMARY" diff --git a/lua/spec/snapshots/LeagueIcon.generate_copy_paste_gen.png b/lua/spec/snapshots/LeagueIcon.generate_copy_paste_gen.png index f7d4f4f5f82..6f68f56945b 100644 Binary files a/lua/spec/snapshots/LeagueIcon.generate_copy_paste_gen.png and b/lua/spec/snapshots/LeagueIcon.generate_copy_paste_gen.png differ diff --git a/lua/spec/snapshots/Slider.png b/lua/spec/snapshots/Slider.png index 37491c62b57..94a40a95c16 100644 Binary files a/lua/spec/snapshots/Slider.png and b/lua/spec/snapshots/Slider.png differ diff --git a/lua/spec/snapshots/date range display.png b/lua/spec/snapshots/date range display.png index 10617de2c64..f8434c2e52b 100644 Binary files a/lua/spec/snapshots/date range display.png and b/lua/spec/snapshots/date range display.png differ diff --git a/lua/spec/snapshots/dota2 rankings.png b/lua/spec/snapshots/dota2 rankings.png index e61cb14992e..7344bbe698b 100644 Binary files a/lua/spec/snapshots/dota2 rankings.png and b/lua/spec/snapshots/dota2 rankings.png differ diff --git a/lua/spec/snapshots/infobox_league_apexlegends.png b/lua/spec/snapshots/infobox_league_apexlegends.png index 233f01a9aeb..ec9eb307777 100644 Binary files a/lua/spec/snapshots/infobox_league_apexlegends.png and b/lua/spec/snapshots/infobox_league_apexlegends.png differ diff --git a/lua/spec/snapshots/infobox_league_counterstrike.png b/lua/spec/snapshots/infobox_league_counterstrike.png index 0341ac21dd6..37475c67aea 100644 Binary files a/lua/spec/snapshots/infobox_league_counterstrike.png and b/lua/spec/snapshots/infobox_league_counterstrike.png differ diff --git a/lua/spec/snapshots/infobox_league_dota2.png b/lua/spec/snapshots/infobox_league_dota2.png index 5bc43aff940..c6a5d9ccaec 100644 Binary files a/lua/spec/snapshots/infobox_league_dota2.png and b/lua/spec/snapshots/infobox_league_dota2.png differ diff --git a/lua/spec/snapshots/infobox_league_leagueoflegends.png b/lua/spec/snapshots/infobox_league_leagueoflegends.png index 0341ac21dd6..37475c67aea 100644 Binary files a/lua/spec/snapshots/infobox_league_leagueoflegends.png and b/lua/spec/snapshots/infobox_league_leagueoflegends.png differ diff --git a/lua/spec/snapshots/infobox_league_mobilelegends.png b/lua/spec/snapshots/infobox_league_mobilelegends.png index 0341ac21dd6..37475c67aea 100644 Binary files a/lua/spec/snapshots/infobox_league_mobilelegends.png and b/lua/spec/snapshots/infobox_league_mobilelegends.png differ diff --git a/lua/spec/snapshots/infobox_league_overwatch.png b/lua/spec/snapshots/infobox_league_overwatch.png index 5f4f4242ddd..14647828865 100644 Binary files a/lua/spec/snapshots/infobox_league_overwatch.png and b/lua/spec/snapshots/infobox_league_overwatch.png differ diff --git a/lua/spec/snapshots/infobox_league_rainbowsix.png b/lua/spec/snapshots/infobox_league_rainbowsix.png index 00bbe611b18..16a0ec19163 100644 Binary files a/lua/spec/snapshots/infobox_league_rainbowsix.png and b/lua/spec/snapshots/infobox_league_rainbowsix.png differ diff --git a/lua/spec/snapshots/infobox_league_rocketleague.png b/lua/spec/snapshots/infobox_league_rocketleague.png index 0341ac21dd6..37475c67aea 100644 Binary files a/lua/spec/snapshots/infobox_league_rocketleague.png and b/lua/spec/snapshots/infobox_league_rocketleague.png differ diff --git a/lua/spec/snapshots/infobox_league_starcraft2.png b/lua/spec/snapshots/infobox_league_starcraft2.png index 365beb690b6..ac1e3bb8781 100644 Binary files a/lua/spec/snapshots/infobox_league_starcraft2.png and b/lua/spec/snapshots/infobox_league_starcraft2.png differ diff --git a/lua/spec/snapshots/infobox_league_valorant.png b/lua/spec/snapshots/infobox_league_valorant.png index 0341ac21dd6..37475c67aea 100644 Binary files a/lua/spec/snapshots/infobox_league_valorant.png and b/lua/spec/snapshots/infobox_league_valorant.png differ diff --git a/lua/spec/snapshots/match2_matchlist_smoke_apexlegends.png b/lua/spec/snapshots/match2_matchlist_smoke_apexlegends.png index cfd22ef5f92..3b43905bca2 100644 Binary files a/lua/spec/snapshots/match2_matchlist_smoke_apexlegends.png and b/lua/spec/snapshots/match2_matchlist_smoke_apexlegends.png differ diff --git a/lua/spec/snapshots/match2_matchlist_smoke_counterstrike.png b/lua/spec/snapshots/match2_matchlist_smoke_counterstrike.png index 12cd3c3c1a2..8a95c591ea3 100644 Binary files a/lua/spec/snapshots/match2_matchlist_smoke_counterstrike.png and b/lua/spec/snapshots/match2_matchlist_smoke_counterstrike.png differ diff --git a/lua/spec/snapshots/match2_matchlist_smoke_dota2.png b/lua/spec/snapshots/match2_matchlist_smoke_dota2.png index 12cd3c3c1a2..8a95c591ea3 100644 Binary files a/lua/spec/snapshots/match2_matchlist_smoke_dota2.png and b/lua/spec/snapshots/match2_matchlist_smoke_dota2.png differ diff --git a/lua/spec/snapshots/match2_matchlist_smoke_leagueoflegends.png b/lua/spec/snapshots/match2_matchlist_smoke_leagueoflegends.png index 12cd3c3c1a2..8a95c591ea3 100644 Binary files a/lua/spec/snapshots/match2_matchlist_smoke_leagueoflegends.png and b/lua/spec/snapshots/match2_matchlist_smoke_leagueoflegends.png differ diff --git a/lua/spec/snapshots/match2_matchlist_smoke_mobilelegends.png b/lua/spec/snapshots/match2_matchlist_smoke_mobilelegends.png index 12cd3c3c1a2..8a95c591ea3 100644 Binary files a/lua/spec/snapshots/match2_matchlist_smoke_mobilelegends.png and b/lua/spec/snapshots/match2_matchlist_smoke_mobilelegends.png differ diff --git a/lua/spec/snapshots/match2_matchlist_smoke_overwatch.png b/lua/spec/snapshots/match2_matchlist_smoke_overwatch.png index 12cd3c3c1a2..8a95c591ea3 100644 Binary files a/lua/spec/snapshots/match2_matchlist_smoke_overwatch.png and b/lua/spec/snapshots/match2_matchlist_smoke_overwatch.png differ diff --git a/lua/spec/snapshots/match2_matchlist_smoke_rainbowsix.png b/lua/spec/snapshots/match2_matchlist_smoke_rainbowsix.png index 12cd3c3c1a2..8a95c591ea3 100644 Binary files a/lua/spec/snapshots/match2_matchlist_smoke_rainbowsix.png and b/lua/spec/snapshots/match2_matchlist_smoke_rainbowsix.png differ diff --git a/lua/spec/snapshots/match2_matchlist_smoke_rocketleague.png b/lua/spec/snapshots/match2_matchlist_smoke_rocketleague.png index 12cd3c3c1a2..8a95c591ea3 100644 Binary files a/lua/spec/snapshots/match2_matchlist_smoke_rocketleague.png and b/lua/spec/snapshots/match2_matchlist_smoke_rocketleague.png differ diff --git a/lua/spec/snapshots/match2_matchlist_smoke_starcraft2.png b/lua/spec/snapshots/match2_matchlist_smoke_starcraft2.png index 12cd3c3c1a2..8a95c591ea3 100644 Binary files a/lua/spec/snapshots/match2_matchlist_smoke_starcraft2.png and b/lua/spec/snapshots/match2_matchlist_smoke_starcraft2.png differ diff --git a/lua/spec/snapshots/match2_matchlist_smoke_valorant.png b/lua/spec/snapshots/match2_matchlist_smoke_valorant.png index 12cd3c3c1a2..8a95c591ea3 100644 Binary files a/lua/spec/snapshots/match2_matchlist_smoke_valorant.png and b/lua/spec/snapshots/match2_matchlist_smoke_valorant.png differ diff --git a/lua/spec/snapshots/prize_pool.png b/lua/spec/snapshots/prize_pool.png index 947f6dda138..a73e2b72927 100644 Binary files a/lua/spec/snapshots/prize_pool.png and b/lua/spec/snapshots/prize_pool.png differ diff --git a/lua/spec/snapshots/prize_pool_club_share.png b/lua/spec/snapshots/prize_pool_club_share.png index 7dd8705222f..31d09fbca84 100644 Binary files a/lua/spec/snapshots/prize_pool_club_share.png and b/lua/spec/snapshots/prize_pool_club_share.png differ diff --git a/lua/spec/snapshots/squad_row_apexlegends.png b/lua/spec/snapshots/squad_row_apexlegends.png index 29c73b70fba..2c2076d1835 100644 Binary files a/lua/spec/snapshots/squad_row_apexlegends.png and b/lua/spec/snapshots/squad_row_apexlegends.png differ diff --git a/lua/spec/snapshots/squad_row_counterstrike.png b/lua/spec/snapshots/squad_row_counterstrike.png index 29c73b70fba..2c2076d1835 100644 Binary files a/lua/spec/snapshots/squad_row_counterstrike.png and b/lua/spec/snapshots/squad_row_counterstrike.png differ diff --git a/lua/spec/snapshots/squad_row_dota2.png b/lua/spec/snapshots/squad_row_dota2.png index 29c73b70fba..2c2076d1835 100644 Binary files a/lua/spec/snapshots/squad_row_dota2.png and b/lua/spec/snapshots/squad_row_dota2.png differ diff --git a/lua/spec/snapshots/squad_row_leagueoflegends.png b/lua/spec/snapshots/squad_row_leagueoflegends.png index 29c73b70fba..2c2076d1835 100644 Binary files a/lua/spec/snapshots/squad_row_leagueoflegends.png and b/lua/spec/snapshots/squad_row_leagueoflegends.png differ diff --git a/lua/spec/snapshots/squad_row_mobilelegends.png b/lua/spec/snapshots/squad_row_mobilelegends.png index 29c73b70fba..2c2076d1835 100644 Binary files a/lua/spec/snapshots/squad_row_mobilelegends.png and b/lua/spec/snapshots/squad_row_mobilelegends.png differ diff --git a/lua/spec/snapshots/squad_row_overwatch.png b/lua/spec/snapshots/squad_row_overwatch.png index 29c73b70fba..2c2076d1835 100644 Binary files a/lua/spec/snapshots/squad_row_overwatch.png and b/lua/spec/snapshots/squad_row_overwatch.png differ diff --git a/lua/spec/snapshots/squad_row_rocketleague.png b/lua/spec/snapshots/squad_row_rocketleague.png index 29c73b70fba..2c2076d1835 100644 Binary files a/lua/spec/snapshots/squad_row_rocketleague.png and b/lua/spec/snapshots/squad_row_rocketleague.png differ diff --git a/lua/spec/snapshots/squad_row_starcraft2.png b/lua/spec/snapshots/squad_row_starcraft2.png index 29c73b70fba..2c2076d1835 100644 Binary files a/lua/spec/snapshots/squad_row_starcraft2.png and b/lua/spec/snapshots/squad_row_starcraft2.png differ diff --git a/lua/spec/snapshots/squad_row_valorant.png b/lua/spec/snapshots/squad_row_valorant.png index 29c73b70fba..2c2076d1835 100644 Binary files a/lua/spec/snapshots/squad_row_valorant.png and b/lua/spec/snapshots/squad_row_valorant.png differ diff --git a/lua/spec/snapshots/standings_ffa.png b/lua/spec/snapshots/standings_ffa.png index 8fe0e264b85..3cdb369b0b7 100644 Binary files a/lua/spec/snapshots/standings_ffa.png and b/lua/spec/snapshots/standings_ffa.png differ diff --git a/lua/spec/snapshots/standings_legend.png b/lua/spec/snapshots/standings_legend.png index a6b06d35b8b..ef609d09392 100644 Binary files a/lua/spec/snapshots/standings_legend.png and b/lua/spec/snapshots/standings_legend.png differ diff --git a/lua/spec/snapshots/standings_swiss.png b/lua/spec/snapshots/standings_swiss.png index dd84f596f67..e5e3b5dfb96 100644 Binary files a/lua/spec/snapshots/standings_swiss.png and b/lua/spec/snapshots/standings_swiss.png differ diff --git a/lua/spec/snapshots/tabs_dynamic_variants.png b/lua/spec/snapshots/tabs_dynamic_variants.png index 2156aa867ef..5740db67c30 100644 Binary files a/lua/spec/snapshots/tabs_dynamic_variants.png and b/lua/spec/snapshots/tabs_dynamic_variants.png differ diff --git a/lua/spec/snapshots/team_participant.png b/lua/spec/snapshots/team_participant.png index a473f1b388b..fc47b8d6dbe 100644 Binary files a/lua/spec/snapshots/team_participant.png and b/lua/spec/snapshots/team_participant.png differ diff --git a/lua/spec/snapshots/teamcard_legacy.png b/lua/spec/snapshots/teamcard_legacy.png index a72353f8a68..54031ac6630 100644 Binary files a/lua/spec/snapshots/teamcard_legacy.png and b/lua/spec/snapshots/teamcard_legacy.png differ diff --git a/lua/spec/snapshots/transfer_row_apexlegends.png b/lua/spec/snapshots/transfer_row_apexlegends.png index dcb0ad80dff..a1c3644787c 100644 Binary files a/lua/spec/snapshots/transfer_row_apexlegends.png and b/lua/spec/snapshots/transfer_row_apexlegends.png differ diff --git a/lua/spec/snapshots/transfer_row_counterstrike.png b/lua/spec/snapshots/transfer_row_counterstrike.png index dcb0ad80dff..a1c3644787c 100644 Binary files a/lua/spec/snapshots/transfer_row_counterstrike.png and b/lua/spec/snapshots/transfer_row_counterstrike.png differ diff --git a/lua/spec/snapshots/transfer_row_dota2.png b/lua/spec/snapshots/transfer_row_dota2.png index dcb0ad80dff..a1c3644787c 100644 Binary files a/lua/spec/snapshots/transfer_row_dota2.png and b/lua/spec/snapshots/transfer_row_dota2.png differ diff --git a/lua/spec/snapshots/transfer_row_leagueoflegends.png b/lua/spec/snapshots/transfer_row_leagueoflegends.png index 725d6bd6dfb..5d4f88f682a 100644 Binary files a/lua/spec/snapshots/transfer_row_leagueoflegends.png and b/lua/spec/snapshots/transfer_row_leagueoflegends.png differ diff --git a/lua/spec/snapshots/transfer_row_mobilelegends.png b/lua/spec/snapshots/transfer_row_mobilelegends.png index 725d6bd6dfb..5d4f88f682a 100644 Binary files a/lua/spec/snapshots/transfer_row_mobilelegends.png and b/lua/spec/snapshots/transfer_row_mobilelegends.png differ diff --git a/lua/spec/snapshots/transfer_row_overwatch.png b/lua/spec/snapshots/transfer_row_overwatch.png index dcb0ad80dff..a1c3644787c 100644 Binary files a/lua/spec/snapshots/transfer_row_overwatch.png and b/lua/spec/snapshots/transfer_row_overwatch.png differ diff --git a/lua/spec/snapshots/transfer_row_rainbowsix.png b/lua/spec/snapshots/transfer_row_rainbowsix.png index dcb0ad80dff..a1c3644787c 100644 Binary files a/lua/spec/snapshots/transfer_row_rainbowsix.png and b/lua/spec/snapshots/transfer_row_rainbowsix.png differ diff --git a/lua/spec/snapshots/transfer_row_rocketleague.png b/lua/spec/snapshots/transfer_row_rocketleague.png index 725d6bd6dfb..5d4f88f682a 100644 Binary files a/lua/spec/snapshots/transfer_row_rocketleague.png and b/lua/spec/snapshots/transfer_row_rocketleague.png differ diff --git a/lua/spec/snapshots/transfer_row_starcraft2.png b/lua/spec/snapshots/transfer_row_starcraft2.png index e5ab23fa7fc..01e6103e469 100644 Binary files a/lua/spec/snapshots/transfer_row_starcraft2.png and b/lua/spec/snapshots/transfer_row_starcraft2.png differ diff --git a/lua/spec/snapshots/transfer_row_valorant.png b/lua/spec/snapshots/transfer_row_valorant.png index dcb0ad80dff..a1c3644787c 100644 Binary files a/lua/spec/snapshots/transfer_row_valorant.png and b/lua/spec/snapshots/transfer_row_valorant.png differ diff --git a/lua/wikis/commons/Widget/Table2/Row.lua b/lua/wikis/commons/Widget/Table2/Row.lua index 47125358dea..b65add216f0 100644 --- a/lua/wikis/commons/Widget/Table2/Row.lua +++ b/lua/wikis/commons/Widget/Table2/Row.lua @@ -140,4 +140,4 @@ end return Component.component( Table2Row -) \ No newline at end of file +) diff --git a/scripts/customization_metrics.py b/scripts/customization_metrics.py new file mode 100644 index 00000000000..16c17204f1d --- /dev/null +++ b/scripts/customization_metrics.py @@ -0,0 +1,171 @@ +"""Measure how much per-wiki customization code lives outside lua/wikis/commons. + +Everything in this repo is standardized; what varies is how much a wiki has to +override to get the behaviour it wants. Override code is the part that costs +maintenance and has to be carried forward, so it is the number worth watching. +Declarative data and config, and legacy shims, are reported separately. + +Classification is by file *content*, not filename -- `GetMatchGroupCopyPaste/ +wiki.lua`, `FilterButtons/Config.lua` and `NotabilityChecker/config.lua` all +look declarative and are not. A file is override code when it defines a +non-local function, or when a `local function` is exported via a `return` +statement (the widget pattern: `local function X` ... `return wrap(X)`). +Purely-local helpers inside an otherwise declarative file do not promote it. + +Shares are against the whole of lua/wikis (commons included), so "override code +is 37% of all Lua" is answerable. Vendored code, type stubs, specs and test +assets under lua/ are excluded from the denominator -- they are not the product. + +Prints a Markdown table, optionally with deltas against a second tree. Pass +--raw for `key=value` output instead. + +Usage: + python scripts/customization_metrics.py [wikis-root] [--base BASE_ROOT] [--raw] +""" + +import argparse +import pathlib +import re +import sys + +DEFAULT_ROOT = "lua/wikis" +COMMONS = "commons" + +EXPORTED_FUNCTION = re.compile(r"\bfunction\b") +LOCAL_FUNCTION = re.compile( + r"^[ \t]*local[ \t]+(?:function|[A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*function)" +) +LOCAL_FUNCTION_NAME = re.compile(r"^[ \t]*local[ \t]+function[ \t]+([A-Za-z_]\w*)") +RETURN_STATEMENT = re.compile(r"^[ \t]*return\b") + + +def is_override_code(lines): + """True when the file exposes behaviour rather than just data or config.""" + local_names = [] + returns = [] + for line in lines: + if not EXPORTED_FUNCTION.search(line): + if RETURN_STATEMENT.match(line): + returns.append(line) + continue + if not LOCAL_FUNCTION.match(line): + return True # a non-local function definition + name = LOCAL_FUNCTION_NAME.match(line) + if name: + local_names.append(name.group(1)) + + # A local function handed out through `return` is the module's interface. + return any(name in line for line in returns for name in local_names) + + +def measure(root): + declarative = legacy = override = commons = 0 + + for path in sorted(root.rglob("*.lua")): + text = path.read_text(encoding="utf-8", errors="replace") + count = len(text.splitlines()) + + # commons is the shared implementation, not per-wiki customization. It is + # still counted, so shares have the whole of lua/wikis as denominator. + if COMMONS in path.parts: + commons += count + # Substring, not a path component: legacy lives in directories + # (TeamCard/Legacy/Custom.lua) *and* in filenames (Match/Legacy.lua). + # `"Legacy" in path.parts` would miss the latter -- 52 of 78 files, + # moving 6279 lines out of legacy and into override code. + elif "Legacy" in path.as_posix(): + legacy += count + elif is_override_code(text.splitlines()): + override += count + else: + declarative += count + + per_wiki = override + declarative + legacy + total = per_wiki + commons + + def share(count): + return round(count / total * 100, 2) if total else 0.0 + + return { + "override_code": override, + "declarative": declarative, + "legacy": legacy, + "per_wiki_total": per_wiki, + "commons": commons, + "total": total, + "override_code_pct": share(override), + "declarative_pct": share(declarative), + "legacy_pct": share(legacy), + "per_wiki_total_pct": share(per_wiki), + "commons_pct": share(commons), + } + + +def count_cell(value, base): + return f"{value}" if base is None else f"{value} ({value - base:+d})" + + +def pct_cell(value, base): + if base is None: + return f"{value:.2f}%" + return f"{value:.2f}% ({value - base:+.2f} pp)" + + +def table(head, base): + def of(key): + return None if base is None else base[key] + + rows = [ + ("**Override code**", "override_code", "override_code_pct"), + ("Declarative (data + config)", "declarative", "declarative_pct"), + ("Legacy", "legacy", "legacy_pct"), + ("Per-wiki total", "per_wiki_total", "per_wiki_total_pct"), + ("lua/wikis/commons (shared)", "commons", "commons_pct"), + ] + lines = [ + "| Per-wiki customization | LOC | Share of all Lua |", + "|-|-|-|", + ] + for label, loc_key, pct_key in rows: + loc = count_cell(head[loc_key], of(loc_key)) + pct = pct_cell(head[pct_key], of(pct_key)) + if label.startswith("**"): + loc, pct = f"**{loc}**", f"**{pct}**" + lines.append(f"| {label} | {loc} | {pct} |") + lines.append(f"| All of lua/wikis | {count_cell(head['total'], of('total'))} | |") + if base is None: + lines.append("| | _no baseline available; deltas omitted_ | |") + return lines + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("root", nargs="?", type=pathlib.Path, default=DEFAULT_ROOT) + parser.add_argument( + "--base", type=pathlib.Path, help="second tree to compare against" + ) + parser.add_argument("--raw", action="store_true", help="print key=value instead") + args = parser.parse_args() + + if not args.root.is_dir(): + print(f"::error::wikis root not found: {args.root}") + return 1 + + head = measure(args.root) + if args.raw: + for key, value in head.items(): + print(f"{key}={value}") + return 0 + + base = None + if args.base: + if args.base.is_dir(): + base = measure(args.base) + else: + print(f"::warning::base root not found, omitting deltas: {args.base}") + print("\n".join(table(head, base))) + return 0 + + +if __name__ == "__main__": + sys.exit(main())