Files
flutter_wisetronic/tools/nullfix.py

241 lines
9.5 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','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} ===')