Files
flutter_wisetronic/tools/nullfix.py

196 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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','await','yield','break','continue','assert','new','const',
'final','var','late','switch','case','default','if','else','for','while',
'do','try','catch','finally','in','is','as','super','this'}
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_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)
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} ===')