#!/usr/bin/env python3
"""
nullfix.py — dart analyze 驱动的空安全批量修复（保守、带语法损坏自检）。

处理的规则（全部由 analyzer 位置驱动，逐轮迭代）：
  1. unchecked_use_of_nullable_value  -> 在接收者后插 '!'（属性/方法/[]/运算符）
  2. not_initialized_non_nullable_instance_field / _variable
     -> 字段/变量改为可空（在类型后插 '?'），跳过 return/this./关键词行
  3. missing_default_value_for_parameter
     -> 参数改为可空（类型后插 '?'），跳过 field-formal this.x

不处理（需语义判断）: argument_type_not_assignable / invalid_assignment
  （这两类留给后续：多为 nullable 传给非空形参，常伴随 1/2 解决后自动减少）

每轮结束检测 expected_token / missing_identifier（语法损坏），有则中止。

用法: python3 tools/nullfix.py [file] [--max-rounds N]
"""
import re, subprocess, sys, os

ROUNDS = 40
args=[]
i=0
while i<len(sys.argv):
    a=sys.argv[i]
    if a=='--max-rounds': ROUNDS=int(sys.argv[i+1]); i+=2; continue
    if not a.endswith('nullfix.py'): args.append(a)
    i+=1
TARGET = args[0] if args else None

ERR = re.compile(r'\s+error\s+-\s+([\w/.\-]+):(\d+):(\d+)\s+-\s+(.*?)\s+-\s+([a-z_]+)\s*$')
PROP = re.compile(r"The property '([^']+)'")
METH = re.compile(r"The method '([^']+)'")
OPR  = re.compile(r"The operator '([^']+)'")
NAME = re.compile(r"'([^']+)'")
KEYWORDS = {'return','throw','yield','await','break','continue'}

def analyze():
    out = subprocess.run(['dart','analyze','lib'], capture_output=True, text=True).stdout
    errs=[]; corruption=0
    for ln in out.splitlines():
        if 'expected_token' in ln or 'missing_identifier' in ln:
            corruption += 1
        m = ERR.match(ln)
        if m:
            p,l,c,msg,r=m.groups(); errs.append((p,int(l),int(c),msg,r))
    if TARGET:
        t=TARGET.replace('lib/','')
        errs=[e for e in errs if e[0].endswith(t)]
    return errs, corruption, out

def classify_use(msg):
    m=PROP.search(msg)
    if m: return (m.group(1),'prop')
    m=METH.search(msg)
    if m: return (None,'index') if m.group(1)=='[]' else (m.group(1),'meth')
    m=OPR.search(msg)
    if m: return (m.group(1),'op')
    return None

def find_name_col(line, name, col0):
    """在 line 上找 name 的出现，返回最接近 col0 的起始 index；找不到 None。"""
    best=None
    for mm in re.finditer(r'(?<![\w])'+re.escape(name)+r'(?!\w)', line):
        d=abs(mm.start()-col0)
        if best is None or d<best[0]: best=(d,mm.start())
    return best[1] if best else None

def insert_bang(line, col0, name, kind):
    if kind in ('prop','meth') and name:
        pat = re.compile(r'(?<!!)\.' + re.escape(name) + r'\b')
        best=None
        for mm in pat.finditer(line):
            d=abs(mm.start()-col0)
            if best is None or d<best[0]: best=(d,mm)
        if not best: return None
        idx=best[1].start(); j=idx-1
        if j<0: return None
        if line[j]=='!': return None
        if not (line[j].isalnum() or line[j] in '_)?]'): return None
        return line[:idx]+'!'+line[idx:]
    if kind=='index':
        best=None
        for i,ch in enumerate(line):
            if ch=='[':
                d=abs(i-col0)
                if best is None or d<best[0]: best=(d,i)
        if not best: return None
        idx=best[1]; j=idx-1
        if j<0 or line[j]=='!': return None
        if line[j].isalnum() or line[j] in '_)?]': return line[:idx]+'!'+line[idx:]
        return None
    if kind=='op':
        for mm in re.finditer(re.escape(name), line):
            idx=mm.start(); j=idx-1
            while j>=0 and line[j]==' ': j-=1
            if j<0 or line[j]=='!': continue
            if line[j].isalnum() or line[j] in '_)?]': return line[:j+1]+'!'+line[j+1:]
        return None
    return None

def make_local_nullable(lines, name, upto_line):
    """从 upto_line 向上找局部变量声明 'Type name;' 并改可空。"""
    pat = re.compile(r'^(\s+)(final\s+|const\s+)?(static\s+)?([\w<>?,\s\.]+?)\s+'+re.escape(name)+r'\s*;\s*$')
    for i in range(upto_line-1, -1, -1):
        L=lines[i]
        m=pat.match(L)
        if not m: continue
        indent, fmod, smod, typ = m.groups()
        if typ in ('var',) or '?' in typ or '=' in L: continue
        return i, f"{indent}{fmod or ''}{smod or ''}{typ}? {name};"
        break
    return None, None

def insert_arg_bang(line, col0):
    """在参数值末尾插 '!'（仅限简单值：标识符/成员/调用/下标）。"""
    depth=0; end=None
    for i in range(col0, len(line)):
        ch=line[i]
        if ch in '([{': depth+=1
        elif ch in ')]}':
            if depth==0: end=i; break
            depth-=1
        elif ch in ',;}' and depth==0: end=i; break
    if end is None: end=len(line)
    # 值的实际末尾（跳过空白）
    j=end-1
    while j>=col0 and line[j] in ' \t': j-=1
    if j<col0: return None
    if line[j]=='!': return None
    val=line[col0:j+1].strip()
    if not val: return None
    if val[0] in '0123456789"\'': return None      # 字面量跳过
    if val in ('true','false','null'): return None
    if not re.fullmatch(r'[\w.\[\]()!]+', val): return None  # 含运算符/空格跳过
    return line[:j+1]+'!'+line[j+1:]

def make_field_nullable(lines, name):
    """找字段声明 'Type name;' 并在类型后加 '?'。返回 (行索引, 新行) 或 (None,None)。"""
    pat = re.compile(r'^(\s+(?:final\s+|const\s+)?(?:static\s+)?)(.+)\s+'+re.escape(name)+r';\s*$')
    for i,L in enumerate(lines):
        m=pat.match(L)
        if not m: continue
        mod, typ = m.group(1), m.group(2).rstrip()
        if '?' in typ or '=' in L: continue
        return i, f"{mod}{typ}? {name};"
    return None, None

def insert_late(line):
    stripped=line.strip()
    if not stripped or stripped.startswith('late '): return None
    head=stripped.split(';')[0]
    if '?' in head or '=' in head: return None  # 已可空/已初始化
    indent=line[:len(line)-len(stripped)]
    if stripped.startswith('static '): return indent+'static late '+stripped[len('static '):]
    if stripped.startswith('external '): return indent+'external late '+stripped[len('external '):]
    return indent+'late '+stripped

def insert_nullable_before_name(line, name, col0):
    """把 'Type name' 改为 'Type? name'。跳过 field-formal(this.)、关键词行。"""
    stripped=line.strip()
    first=stripped.split(' ')[0] if stripped else ''
    if first.rstrip(')').rstrip('(') in KEYWORDS: return None
    idx=find_name_col(line, name, col0)
    if idx is None: return None
    # name 前一个非空字符
    j=idx-1
    while j>=0 and line[j] in ' \t': j-=1
    if j<0: return None
    if line[j]=='.':  return None   # field-formal this.name
    if line[j]=='?':  return None   # 已可空
    # j 应是类型末尾（字母/>/)/]）
    if not (line[j].isalnum() or line[j] in '_>]?)'): return None
    return line[:j+1]+'?'+line[j+1:]

def apply_round(errs):
    changed=0; files={}
    def get(path):
        if path not in files:
            files[path]=open('lib/'+path).read().split('\n')
        return files[path]
    def flush():
        for p,ls in files.items():
            open('lib/'+p,'w').write('\n'.join(ls))
    for path,line,col,msg,rule in errs:
        if not os.path.exists('lib/'+path): continue
        L=get(path)
        if line>len(L): continue
        old=L[line-1]; new=old
        if rule=='unchecked_use_of_nullable_value':
            cls=classify_use(msg)
            if cls: new=insert_bang(old, col-1, cls[0], cls[1])
        elif rule=='missing_default_value_for_parameter':
            nm=NAME.search(msg)
            if nm:
                pname=nm.group(1)
                # field-formal this.x ? 则把字段改可空（跨行）
                if ('this.'+pname) in old:
                    fl=get(path)
                    fi, fnew = make_field_nullable(fl, pname)
                    if fi is not None and fl[fi]!=fnew:
                        fl[fi]=fnew; changed+=1
                else:
                    new=insert_nullable_before_name(old, pname, col-1)
        elif rule in ('not_initialized_non_nullable_instance_field','not_initialized_non_nullable_variable'):
            new=insert_late(old)
        elif rule in ('argument_type_not_assignable','invalid_assignment'):
            new=insert_arg_bang(old, col-1)
        # not_assigned_potentially_non_nullable_local_variable 不自动处理
        # （make_local_nullable 易误改 return/throw 语句，风险高，留 IDE）
        elif False and rule=='not_assigned_potentially_non_nullable_local_variable':
            nm=NAME.search(msg)
            if nm:
                fl=get(path)
                fi, fnew = make_local_nullable(fl, nm.group(1), line)
                if fi is not None and fl[fi]!=fnew:
                    fl[fi]=fnew; changed+=1
        if new and new!=old:
            L[line-1]=new; changed+=1
    flush()
    return changed

rounds=0
while rounds<ROUNDS:
    errs,corruption,_=analyze()
    if corruption:
        print(f'!! 检测到 {corruption} 处语法损坏，中止。请 git diff 检查。'); sys.exit(2)
    if not errs: print('无错误，完成。'); break
    c=apply_round(errs)
    rounds+=1
    print(f'第 {rounds} 轮：修复 {c} 处（剩余 error {len(errs)}）')
    if c==0:
        print('本轮无新增修复，剩余需人工/IDE 处理：')
        from collections import Counter
        cnt=Counter(e[4] for e in errs)
        for r,n in cnt.most_common(8): print(f'   {n:4d}  {r}')
        break

errs,corruption,_=analyze()
print(f'\n=== 完成。剩余 error: {len(errs)} | 语法损坏: {corruption} ===')
