mirror of
https://github.com/newliver666/apk-reverse.git
synced 2026-09-28 21:56:59 +02:00
a1ada96cc3
PR #3 proposed adding mypy and flake8 to CI, with configurations for both. The idea is right and the configurations are kept. Three things about the PR itself needed correcting, and all three were found by running the gate rather than reading the diff: 1. It was written against an older tree. Its own edits reference code that no longer exists there (`unknown, positions, warnings = ...`, a `struct` import at line 857 that is used 600 lines later), so merging it as-is would have removed a live import. The fixes were re-derived against the current tree instead, and the PR is superseded. 2. It added a `paths:` filter to the workflow, watching only `*.py` plus the two lint configs. That would stop `check_refs.py` and `check_routing.py` -- documentation gates -- from running on a documentation-only commit, which is exactly the change they exist to catch. The workflow keeps the PR's `actions/*@v7` bump and gains no filter; the reason is written next to the trigger. 3. `pip install --upgrade pip pytest` was dropped to just the packages, because upgrading pip in CI is not a goal and can fail for reasons unrelated to this repository. What the gate found on the current tree, and what was done about it -- 70 flake8 findings and 3 mypy errors, none of which were cosmetic-only: F401 (23) unused imports, removed. These were safe to delete because the import statement itself cannot have a side effect that matters here. F841 (12) unused bindings, handled three ways rather than one. Dead code was deleted (`mt_mcp_probe.print_waiting_help`'s `host`, `repack.py`'s `signer_jar`); a computed value that was then forgotten was **wired up** (`rasc_build.py` now names the linker target in its build-failure hint instead of discarding it); and a binding that is intentional kept its place with `# noqa: F841` and the reason -- `java2c_probe` reads dex header fields one by one against the format spec, and deleting three of them to please a linter would have made the parser stop lining up with the format it implements. E741 (14) the ambiguous loop name `l`, renamed with an AST pass that resolves each binding to its own scope. A regex rename would have been wrong: several scopes bind their own `l`. That pass had a defect of its own -- it treated a comprehension target as living in the comprehension scope while leaving references to it in the enclosing one -- which produced seven `undefined name 'l'`. Found by running flake8, fixed, and the counting bug is documented in `check_routing.py`, whose own `tables()` had the same class of error in the other direction. style continuation indents, blank lines between top-level definitions, and lines over length. The line limit is 160, not 120, and the number is a **measurement**: this repository's secret-scanner rule table holds raw regex source of 148 and 156 characters, which cannot be split without changing the pattern. mypy (3) two missing annotations on module-level containers, and one optional-dependency rebind (`dexutil = None` after `except ImportError`) that is the repository's own idiom and now says so with a `type: ignore[assignment]` and the reason. No code was reordered to satisfy the checker. Verified on a venv containing only `mypy` and `flake8` -- the poorest environment the configuration must work in, and the one CI uses: **flake8 reports 0, mypy reports "Success: no issues found in 55 source files"**. Behaviour is unchanged: 625 passed / 163 skipped / 25 xfailed, the same five gates green (check_repo 0 problem, check_refs 0 dangling, check_routing coherent, check_budget 0 failure, check_commands 0 drift), and every key script still prints help. .flake8 new, with the line-length and complexity numbers and their reasons mypy.ini new; optional imports declared, nothing excluded from the analysis ci.yml `actions/*@v7`; new `lint` job on one interpreter; no path filter 30 files the findings above
179 lines
7.1 KiB
Python
179 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Keep `skills/apk-reverse/references/routing.md` coherent with `SKILL.md` and with the tree.
|
|
|
|
Why this exists: `SKILL.md` loads in full whenever this skill activates, so it carries only what has
|
|
to be one hop there -- the symptom index (recognising a failure must not become a two-hop lookup) and
|
|
the four gates with their pass criteria. The *inventory* lookups -- every reference file with when to
|
|
load it, every script with what it does -- live in `references/routing.md` and load on demand.
|
|
|
|
That split creates one risk: two tables that used to be one. This script removes it by **comparing**
|
|
the tables instead of generating one from the other:
|
|
|
|
* the **symptom index** has a single home, `SKILL.md`; `routing.md` keeps a mirror so a reader gets
|
|
all three tables in one load, and `--check` fails when the mirror and the live table differ;
|
|
* the **reference and script indexes** have a single home, `routing.md`;
|
|
* every file under `references/` must be named in the routing reference table, and every script in
|
|
`scripts/` must be named in the routing script table -- an inventory that has drifted from the
|
|
directory is worse than no inventory, because it reads as complete.
|
|
|
|
Usage:
|
|
python check_routing.py # report coherence; exit 1 on any problem
|
|
python check_routing.py --json
|
|
"""
|
|
import argparse
|
|
import glob
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
ROOT = os.path.dirname(os.path.abspath(__file__))
|
|
SKILL_DIR = os.path.join(ROOT, 'skills', 'apk-reverse')
|
|
SKILL = os.path.join(SKILL_DIR, 'SKILL.md')
|
|
REFS = os.path.join(SKILL_DIR, 'references')
|
|
SCRIPTS = os.path.join(SKILL_DIR, 'scripts')
|
|
ROUTE = os.path.join(REFS, 'routing.md')
|
|
|
|
# A markdown table separator row: pipes, dashes, colons and spaces, and at least one dash. Tested on
|
|
# the row's *cells*, not the whole line -- every data row also starts and ends with `|`, so any
|
|
# whole-line pattern loose enough to accept a separator also accepts every data row, and the reverse
|
|
# mistake drops every data row and reports an intact inventory as broken.
|
|
|
|
|
|
def is_separator(row):
|
|
cells = [c for c in row.strip().strip('|').split('|')]
|
|
return bool(cells) and all(c.strip() and set(c.strip()) <= set('-:') for c in cells)
|
|
|
|
|
|
# Headings are matched by prefix: they contain an em dash, and comparing against an ASCII hyphen
|
|
# silently reports "table not found" instead of failing.
|
|
PREFIXES = {
|
|
'symptom': 'Symptom index',
|
|
'reference': 'Reference index',
|
|
'script': 'Script index',
|
|
}
|
|
|
|
|
|
def read(path):
|
|
with open(path, encoding='utf-8') as fh:
|
|
return fh.read()
|
|
|
|
|
|
def tables(text):
|
|
"""{heading: [data rows]} -- data rows exclude the header row.
|
|
|
|
The separator row is identified by its characters (`-`, `|`, `:`, spaces only). Do **not** use
|
|
`set(row) > set('|-: ')` to decide this: set comparison is not a character test, and every row
|
|
whose content happens to be a superset of those characters gets dropped -- which once classified
|
|
43 of 53 symptom rows as "not a table row" and made the checker report the inventory as broken
|
|
while it was intact. This file's own failure mode, found by a row count that did not add up.
|
|
"""
|
|
out = {}
|
|
cur = None
|
|
for ln in text.split('\n'):
|
|
if ln.startswith('## '):
|
|
cur = ln[3:].strip()
|
|
out.setdefault(cur, [])
|
|
elif cur is not None and ln.startswith('|'):
|
|
out[cur].append(ln)
|
|
result = {}
|
|
for key, rows in out.items():
|
|
data = [r for r in rows if not is_separator(r)]
|
|
result[key] = data[1:] if data else [] # drop the header row
|
|
return result
|
|
|
|
|
|
def pick(sections, prefix):
|
|
for key, rows in sections.items():
|
|
if key.startswith(prefix):
|
|
return rows
|
|
return None
|
|
|
|
|
|
def cell0(row):
|
|
return row.strip().strip('|').split('|')[0].strip().strip('`* ').lower()
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument('--json', action='store_true')
|
|
args = ap.parse_args()
|
|
|
|
problems = []
|
|
if not os.path.isfile(ROUTE):
|
|
print('FAIL references/routing.md is missing -- the inventory has no home')
|
|
print('RESULT=missing')
|
|
return 1
|
|
|
|
sk = tables(read(SKILL))
|
|
rt = tables(read(ROUTE))
|
|
|
|
live_sym = pick(sk, PREFIXES['symptom']) or []
|
|
mirror_sym = pick(rt, PREFIXES['symptom']) or []
|
|
ref_rows = pick(rt, PREFIXES['reference'])
|
|
scr_rows = pick(rt, PREFIXES['script'])
|
|
|
|
for name, rows in (('symptom (routing.md)', mirror_sym), ('reference', ref_rows),
|
|
('script', scr_rows)):
|
|
if rows is None:
|
|
problems.append('routing.md has no %s table' % name)
|
|
|
|
live_set = {cell0(r) for r in live_sym}
|
|
mirror_set = {cell0(r) for r in mirror_sym}
|
|
if live_set - mirror_set:
|
|
problems.append('routing.md symptom mirror is missing %d row(s) present in SKILL.md: %s'
|
|
% (len(live_set - mirror_set), '; '.join(sorted(live_set - mirror_set))[:180]))
|
|
if mirror_set - live_set:
|
|
problems.append('routing.md symptom mirror has %d row(s) SKILL.md does not: %s'
|
|
% (len(mirror_set - live_set), '; '.join(sorted(mirror_set - live_set))[:180]))
|
|
|
|
ref_text = ' '.join(r for r in (ref_rows or []))
|
|
ref_files = sorted(os.path.basename(p) for p in glob.glob(os.path.join(REFS, '*.md')))
|
|
unlisted = [f for f in ref_files if f not in ref_text]
|
|
if unlisted:
|
|
problems.append('routing.md reference table does not name %d reference file(s): %s'
|
|
% (len(unlisted), ', '.join(unlisted)))
|
|
|
|
scr_text = ' '.join(r for r in (scr_rows or []))
|
|
scr_files = sorted(f for f in os.listdir(SCRIPTS) if f.endswith(('.py', '.js')))
|
|
unlisted_s = [f for f in scr_files if f not in scr_text]
|
|
if unlisted_s:
|
|
problems.append('routing.md script table does not name %d script(s): %s'
|
|
% (len(unlisted_s), ', '.join(unlisted_s)))
|
|
|
|
report = {
|
|
'skill_symptom_rows': len(live_sym),
|
|
'routing_symptom_rows': len(mirror_sym),
|
|
'routing_reference_rows': len(ref_rows or []),
|
|
'routing_script_rows': len(scr_rows or []),
|
|
'reference_files_in_tree': len(ref_files),
|
|
'scripts_in_tree': len(scr_files),
|
|
'problems': problems,
|
|
'result': 'ok' if not problems else 'incoherent',
|
|
}
|
|
|
|
if args.json:
|
|
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
print('RESULT=%s' % report['result'])
|
|
return 1 if problems else 0
|
|
|
|
print('routing coherence')
|
|
print(' symptom index : SKILL.md %d rows / routing mirror %d rows'
|
|
% (len(live_sym), len(mirror_sym)))
|
|
print(' reference table: %d rows, covering %d reference files' % (len(ref_rows or []), len(ref_files)))
|
|
print(' script table : %d rows, covering %d scripts' % (len(scr_rows or []), len(scr_files)))
|
|
for p in problems:
|
|
print(' FAIL %s' % p)
|
|
print()
|
|
if problems:
|
|
print('== routing: %d problem(s) ==' % len(problems))
|
|
print('RESULT=incoherent')
|
|
return 1
|
|
print('== routing: coherent ==')
|
|
print('RESULT=ok')
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|