#!/usr/bin/env python3 """Static no-undef gate for newOS4 browser scripts. The /agents/ leak fix and the `tc is not defined` crash are the same class of bug: a refactor moves code and leaves a stale identifier behind. node --check can't see it (it's a runtime ReferenceError). ESLint's no-undef can. Cross-file globals (classes/functions declared in *other* bundled files) are collected from top-level declarations across all core+ext files and passed as --global, so only genuinely-undefined identifiers are reported. Usage: python3 scripts/lint-no-undef.py [--changed module-aiassist.js ...] Exit: 0 = clean, 1 = undefined identifiers found """ import os import re import subprocess import sys ROOT = '/var/www/webx11.duckdns.org/spaces/newos4' # eslint is not packaged with newOS4; reuse a local install or bootstrap one: # mkdir -p /tmp/lint && cd /tmp/lint && npm i eslint@8 def _eslint(): import shutil for cand in (os.environ.get('ESLINT_BIN'), '/tmp/lint/node_modules/.bin/eslint', shutil.which('eslint')): if cand and os.path.exists(cand): return cand sys.exit('eslint not found — run: mkdir -p /tmp/lint && cd /tmp/lint && npm i eslint@8') ESLINT = None # resolved in main() CORE = os.path.join(ROOT, 'modules/core') EXT = os.path.join(ROOT, 'modules/ext') DECL = re.compile(r'^(?:class|function|async function|const|let|var)\s+([A-Za-z_$][\w$]*)', re.M) def all_js(d): return [os.path.join(d, f) for f in sorted(os.listdir(d)) if f.endswith('.js') and not f.endswith('.bak')] def collect_globals(files): names = set() for p in files: with open(p, encoding='utf-8', errors='replace') as fh: src = fh.read() names.update(DECL.findall(src)) # window.Foo = ... / globalThis.Foo = ... names.update(re.findall(r'(?:window|globalThis)\.([A-Za-z_$][\w$]*)\s*=', src)) return {n for n in names if len(n) > 1} def main(): global ESLINT ESLINT = _eslint() files = all_js(CORE) + all_js(EXT) args = sys.argv[1:] if args: want = set(args) files = [f for f in files if os.path.basename(f) in want] if not files: print('no matching files'); return 1 globs = collect_globals(all_js(CORE) + all_js(EXT)) cmd = [ESLINT, '--no-eslintrc', '--env', 'browser,es2022', '--rule', '{"no-undef":"error"}', '--global', ' '.join(sorted(globs))] + files r = subprocess.run(cmd, capture_output=True, text=True) out = (r.stdout + r.stderr).strip() findings = re.findall(r"^(.+?):(\d+):(\d+): error '(.+?)' is not defined", out, re.M) if not findings: print(f'✅ no-undef clean — {len(files)} file(s), {len(globs)} cross-file globals declared') return 0 print(f'❌ {len(findings)} undefined identifier(s):\n') for path, line, col, name in findings: print(f' {os.path.relpath(path, ROOT)}:{line}:{col} → {name} is not defined') return 1 if __name__ == '__main__': sys.exit(main())