aboutsummaryrefslogtreecommitdiff
path: root/scripts/lint-no-undef.py
blob: 9f64931e8fa1496695fd904c49b4bb083738e0f6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#!/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())