Repository navigation
Code Analysis Java Unicode
Guava v33.4.8 的 JRE、Android 两个生产源码目录中,公开 Labs bf0a29e7 各报告七份文件需要 Unicode 转义预处理。受影响的文件包括 CharMatcher、Stopwatch、Cut、GeneralRange、InternetDomainName、Chars 和 XmlEscapers。例如 Stopwatch 只在字符串中用转义表示微秒单位;Chars 的两处标记位于 Javadoc,前面各有两个原始反斜线。两种写法都让原版跳过了整份文件。Stopwatch · Chars
Java 在词法分析前处理 Unicode 转义,注释中的换行转义也能露出一条导入。恢复覆盖需要先判断转义是否生效、是否会改变 token 边界,再确认它所在的源码范围。用 JDK 17 编译了字面量、双反斜线、三反斜线、注释结束符、引号、反斜线和标识符对照。另一个注释里的不完整转义被 JDK 判为语法错误。Java 语言规范
Draft #1218(9dc6bea08)直接基于 Labs。它利用已有解析器提供的字符串、字符、文本块和注释范围,只接纳其中保留词法边界的转义,原始源码继续用于定位。换行、引号、注释结束符、反斜线、标识符转义和不完整转义保留诊断。Java 提取修订号升至 2,让基线对照能识别分析器自身的变化。修复 PR #1218
| 单独分析的目录 | 源文件清单 | Labs 跳过 | Draft 跳过 | Labs 函数记录 | Draft 函数记录 |
|---|---|---|---|---|---|
guava/src |
615 | 7 | 0 | 11,774 | 12,126 |
android/guava/src |
614 | 7 | 0 | 11,492 | 11,843 |
- 对恢复的 14 份原始文件,JDK 独立读出了 162 条显式导入和 701 个带方法体的方法。逐条核对导入位置、方法名及起止行,全部与候选一致。候选还记录了两份
XmlEscapers的静态初始化块。这里验证的是源码声明与位置。 - 对原来已解析的目标和原有方法记录逐条复核,通过对照。两个目录共同扫描时,前一案例中的六处同名导入仍标为未解析。SLF4J 对照的事实与原版相同,差异只在提取修订号及其快照标识。
- 两个新增回归在父版本失败,在候选通过。230 项模块测试通过。重复构建得到相同 ZIP;解包 CLI 对两套 Guava 源码共同扫描的原始结果与源码版逐字节一致。另用含换行、引号、注释结束符和不完整转义的文件检查失败隔离,其他文件仍能提供有效事实。
从公开修复逐条核对 Guava 与 JDK 的 863 条声明位置
准备 Git、JDK 17、Node 22、npm 和 Python 3。脚本先用 JDK 的解析步骤记录原始源码的声明范围,再从 GitHub 取得固定的修复提交,运行 CLI 并逐条比对。最终应匹配 162 条显式导入和 701 个方法,完整的 1,229 文件扫描应报告 0 条解析错误。逐条 JDK 结果保存在临时目录的 jdk.tsv。Labs 的三种扫描范围可按前一页复现。Guava 源码范围对照
set -eu
w=$(mktemp -d)
git init -q "$w/guava"
git -C "$w/guava" remote add origin https://github.com/google/guava.git
git -C "$w/guava" fetch -q --no-tags --depth=1 origin f06690fa3e874f65515e8fd338a74d636e2c792f
git -C "$w/guava" checkout -q --detach FETCH_HEAD
cat > "$w/Declarations.java" <<'JAVA'
import java.nio.file.*;
import java.util.*;
import javax.tools.*;
import com.sun.source.tree.*;
import com.sun.source.util.*;
class Declarations {
public static void main(String[] args) throws Exception {
Path root = Path.of(args[0]).toRealPath();
List<Path> paths = Arrays.stream(args).skip(1).map(root::resolve).toList();
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
try (StandardJavaFileManager files = compiler.getStandardFileManager(diagnostics, null, null)) {
JavacTask task = (JavacTask)compiler.getTask(null, files, diagnostics,
List.of("-proc:none", "-encoding", "UTF-8"), null, files.getJavaFileObjectsFromPaths(paths));
SourcePositions ranges = Trees.instance(task).getSourcePositions();
for (CompilationUnitTree unit : task.parse()) {
Path path = root.relativize(Path.of(unit.getSourceFile().toUri()).toRealPath());
new TreePathScanner<Void, Void>() {
void emit(String kind, Tree tree, String name) {
long first = ranges.getStartPosition(unit, tree);
long last = ranges.getEndPosition(unit, tree) - 1;
System.out.printf("%s\t%s\t%d\t%d\t%s%n", kind, path,
unit.getLineMap().getLineNumber(first), unit.getLineMap().getLineNumber(last), name);
}
public Void visitImport(ImportTree node, Void p) {
emit("import", node, node.getQualifiedIdentifier().toString());
return super.visitImport(node, p);
}
public Void visitMethod(MethodTree node, Void p) {
if (node.getBody() != null) {
String name = node.getName().toString();
if (name.equals("<init>")) name = ((ClassTree)getCurrentPath().getParentPath().getLeaf()).getSimpleName().toString();
emit("method", node, name);
}
return super.visitMethod(node, p);
}
}.scan(unit, null);
}
}
for (var diagnostic : diagnostics.getDiagnostics()) {
if (diagnostic.getKind() == Diagnostic.Kind.ERROR) throw new IllegalStateException(diagnostic.toString());
}
}
}
JAVA
python3 - "$w" <<'PY'
from collections import Counter
from pathlib import Path
import subprocess, sys
w = Path(sys.argv[1])
files = [
'base/CharMatcher.java', 'base/Stopwatch.java',
'collect/Cut.java', 'collect/GeneralRange.java',
'net/InternetDomainName.java', 'primitives/Chars.java', 'xml/XmlEscapers.java',
]
paths = [root + '/com/google/common/' + file
for root in ('guava/src', 'android/guava/src') for file in files]
rows = subprocess.check_output(['java', str(w / 'Declarations.java'), str(w / 'guava'), *paths], text=True)
(w / 'jdk.tsv').write_text(rows)
counts = Counter()
for row in rows.splitlines():
kind, file, first, last, name = row.split('\t')
variant = 'Android' if file.startswith('android/') else 'JRE'
counts[variant, kind] += 1
for variant in ('JRE', 'Android'):
print(variant, counts[variant, 'import'], 'imports,', counts[variant, 'method'], 'methods')
print('Per-declaration source locations', w / 'jdk.tsv')
PY
git init -q "$w/archify"
git -C "$w/archify" fetch -q --no-tags --depth=1 https://github.com/tt-a1i/archify.git 9dc6bea08bc820befc4e5d9396692afc87a8fea9
git -C "$w/archify" checkout -q --detach FETCH_HEAD
(cd "$w/archify/archify/modules/code-analysis" && npm ci --ignore-scripts --no-audit --no-fund)
printf '%s\n' '{"include":["guava/src/**/*.java","android/guava/src/**/*.java"]}' > "$w/both.json"
node "$w/archify/archify/bin/archify.mjs" code-analysis check "$w/guava" \
--config "$w/both.json" --language java --fail-on never --json \
--out "$w/facts" > "$w/receipt.json"
python3 - "$w" <<'VERIFY'
from collections import Counter
from pathlib import Path
import hashlib, json, sys
w = Path(sys.argv[1])
data = (w / 'facts' / 'raw-facts.json').read_bytes()
facts = json.loads(data)
assert len(facts['files']) == 1229
assert facts.get('parse_errors', []) == []
imports = Counter((r['from'], r['line'], r['specifier']) for r in facts['imports'])
methods = Counter((r['file'], r['line'], r['endLine'], r['name']) for r in facts['functions'])
matched = Counter()
for row in (w / 'jdk.tsv').read_text().splitlines():
kind, file, first, last, name = row.split('\t')
if kind == 'import':
assert imports[file, int(first), name] == 1, row
else:
assert methods[file, int(first), int(last), name] == 1, row
matched[kind] += 1
assert matched == {'import': 162, 'method': 701}
print('Matched', matched['import'], 'imports and', matched['method'], 'method ranges with JDK')
print('Captured', len(facts['files']), 'Java files with', len(facts.get('parse_errors', [])), 'parse errors')
print('Raw facts sha256', hashlib.sha256(data).hexdigest())
VERIFY