Repository navigation
Code Analysis SLF4J
上游提交 9783320b 给 LoggerFactory 增加了按类名指定实现的路径,并新增一份测试。该历史提交读取的属性名是 slf4j.binding。这里核对的两版分别是它的父提交 aa13da39 和该提交本身。上游差异
用 JDK 17 从各版本的 API、Simple 和 NOP 三个模块直接编译 51 个上游 Java 源文件,再加入一个读结果的小程序。两种提供者类始终在 classpath 中,一次只加入表里指定的服务描述文件。每项使用新的 JVM,分别核对 ServiceLoader 看到的提供者、LoggerFactory 选定的对象和公开方法返回的工厂。
| 放入 classpath 的描述文件 | 属性值 | 变更前实际选择 | 变更后实际选择 |
|---|---|---|---|
| NOP | 未设置 | NOP | NOP |
| NOP | Simple 实现的全限定类名 | NOP | Simple |
| NOP |
java.lang.String,类型不符 |
NOP | NOP |
| NOP | 不存在的类 | NOP | NOP |
| Simple | 未设置 | Simple | Simple |
变更后,显式类名能反射构造提供者时,代码初始化它并提前返回。类名无效时则回到服务发现。两组运行可以使用完全相同的编译产物和 NOP 描述文件,只靠一个系统属性改变实际对象。选择与回退 · 上游对错误类型的测试
- 公开 Labs
bf0a29e7对两个完整源码目录分别捕获 259、260 个 Java 文件,输出 2,415、2,436 条导入或类型引用。后版能定位bind(181–215 行)、loadExplicitlySpecified(217–236 行)和服务发现方法。两版各报告一份迁移文本样本解析失败,路径是slf4j-migrator/jcl/jclcontent.java。 -
LoggerFactory、Simple 和 NOP 提供者到SLF4JServiceProvider的三个显式导入,在两版都指到正确的接口文件。后版还捕获 8 份module-info.java。Simple 的provides … with …产生两条静态类型记录,分别指向接口和实现。由此可以找到实现源码。Simple 模块声明 - 仓库有四份同名的
META-INF/services/org.slf4j.spi.SLF4JServiceProvider描述文件,均在 Java 源文件清单之外。Java 调用记录为 0。要解释一次 classpath 运行,应同时查实际资源目录、系统属性和选择代码。上表采用普通 classpath;JPMS 模块路径和完整 Maven 构建需要各自的运行对照。NOP 描述文件 · Simple 描述文件
用 Git 与 JDK 17 复现十次独立运行
在 Unix shell 配好 java 和 javac,使用下面固定的两版。每行输出依次是服务描述中声明的提供者、实际选中的对象、公开工厂的类名。
set -eu
w=$(mktemp -d)
git init -q "$w/slf4j"
git -C "$w/slf4j" fetch -q --depth=2 https://github.com/qos-ch/slf4j.git 9783320b6c4411a0b120cad9e32cd9a981b1dac2
git -C "$w/slf4j" checkout -q --detach FETCH_HEAD
git -C "$w/slf4j" worktree add -q --detach "$w/parent" 'HEAD^'
cat > "$w/ProviderProbe.java" <<'JAVA'
package org.slf4j;
import java.util.ServiceLoader;
import org.slf4j.spi.SLF4JServiceProvider;
public final class ProviderProbe {
public static void main(String[] args) {
String installed = ServiceLoader.load(SLF4JServiceProvider.class).iterator().next().getClass().getName();
String factory = LoggerFactory.getILoggerFactory().getClass().getName();
String chosen = LoggerFactory.getProvider().getClass().getName();
System.out.println(installed + "|" + chosen + "|" + factory);
}
}
JAVA
for side in parent slf4j; do
src="$w/$side"
out="$w/classes-$side"
mkdir "$out"
(cd "$src"
git ls-files 'slf4j-api/src/main/java/*.java' 'slf4j-simple/src/main/java/*.java' 'slf4j-nop/src/main/java/*.java' > "$w/sources"
javac --release 8 -encoding UTF-8 -d "$out" @"$w/sources" "$w/ProviderProbe.java")
run() {
provider=$1
shift
java "$@" -cp "$out:$src/slf4j-$provider/src/main/resources" org.slf4j.ProviderProbe
}
echo "$side"
run nop
run nop -Dslf4j.binding=org.slf4j.simple.SimpleServiceProvider
run nop -Dslf4j.binding=java.lang.String 2>"$w/$side-type.log"
run nop -Dslf4j.binding=example.MissingProvider 2>"$w/$side-missing.log"
run simple
doneGuava v33.4.8 的 JRE 和 Android 发布物各有一个 guava 模块,分别从 guava/src 和 android/guava/src 取源码。两棵树有 606 个相同的 Java 相对路径,其中 152 对文件内容不同。跟踪某个版本的实现时,先从它的 POM 确认源码目录,再把这个目录传给分析器。JRE 父 POM · Android 父 POM
两边各选了三处导入。Strings.java 第 17、18 行从 Preconditions 静态导入两个方法,两个版本的目标文件内容相同。Files.java 第 29 行导入 ImmutableList,两个版本的目标文件内容不同。用 JDK 17 的标准文件管理器逐一指定源码目录,独立查出这两个类型在各版本的文件路径。对照的是导入所属的源码文件,未执行 Maven 构建。两个静态导入 · 普通导入 · JRE 实现 · Android 实现
公开 Labs bf0a29e7 的扫描范围 |
Java 文件清单 | 所选导入的结果 | 报告跳过的文件 |
|---|---|---|---|
| 两套生产源码一起扫描 | 1,229 | 6/6 标为未解析,均未指定目标文件 | 14 |
| 仅 JRE 源码目录 | 615 | 3/3 指向 JRE 文件,与 JDK 一致 | 7 |
| 仅 Android 源码目录 | 614 | 3/3 指向 Android 文件,与 JDK 一致 | 7 |
两边跳过的是相同的七个相对路径,诊断都指向 Java Unicode 转义预处理。例如 Stopwatch 的微秒字符串和 Cut 的无穷符号都触发了这一限制。即使 CLI 返回成功,这些文件的导入和方法仍缺席。复查覆盖时可以直接看 raw-facts.json 中的 parse_errors。后续已完成这七类文件的逐项验证,并将修复提交为 Draft #1218。Unicode 转义验证与 PR · Stopwatch 源码 · Cut 源码 · Labs 的跳过条件
复现 Guava 的三次扫描,逐处与 JDK 源码查询比对
在 Unix shell 中准备 Git、JDK 17、Node 22、npm 和 Python 3。脚本会拉取两个固定提交,只为临时分析器副本安装锁定依赖。输出包括六个导入位置在两种扫描范围下的结果,以及每次扫描的文件数和解析错误数。
set -eu
w=$(mktemp -d)
pin() {
git init -q "$w/$1"
git -C "$w/$1" fetch -q --no-tags --depth=1 "https://github.com/$2.git" "$3"
git -C "$w/$1" checkout -q --detach FETCH_HEAD
}
pin guava google/guava f06690fa3e874f65515e8fd338a74d636e2c792f
pin archify tt-a1i/archify bf0a29e7664f52973d06a8e6a608df2db1ecbd39
(cd "$w/archify/archify/modules/code-analysis" && npm ci --ignore-scripts --no-audit --no-fund)
cli="$w/archify/archify/bin/archify.mjs"
scan() {
mode=$1
root=$2
shift 2
node "$cli" code-analysis check "$root" --language java --fail-on never \
--json --out "$w/$mode" "$@" > "$w/$mode-receipt.json"
}
printf '%s\n' '{"include":["guava/src/**/*.java","android/guava/src/**/*.java"]}' > "$w/both.json"
scan both "$w/guava" --config "$w/both.json"
scan jre "$w/guava/guava/src"
scan android "$w/guava/android/guava/src"
cat > "$w/SourceLookup.java" <<'JAVA'
import java.nio.file.*;
import java.util.List;
import javax.tools.*;
public class SourceLookup {
public static void main(String[] args) throws Exception {
Path repo = Paths.get(args[0]).toRealPath();
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
for (String root : List.of("guava/src", "android/guava/src")) {
try (StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null)) {
fm.setLocationFromPaths(StandardLocation.SOURCE_PATH, List.of(repo.resolve(root)));
for (String type : List.of("com.google.common.base.Preconditions", "com.google.common.collect.ImmutableList")) {
JavaFileObject source = fm.getJavaFileForInput(StandardLocation.SOURCE_PATH, type, JavaFileObject.Kind.SOURCE);
Path found = Paths.get(source.toUri()).toRealPath();
Path expected = repo.resolve(root).resolve(type.replace('.', '/') + ".java");
if (!Files.isSameFile(found, expected)) throw new AssertionError(found);
System.out.println(root + "|" + type + "|" + repo.relativize(found));
}
}
}
}
}
JAVA
java "$w/SourceLookup.java" "$w/guava" > "$w/jdk.txt"
python3 - "$w" <<'PY'
import json, pathlib, sys
w = pathlib.Path(sys.argv[1])
facts = {k: json.loads((w / k / 'raw-facts.json').read_text()) for k in ('both', 'jre', 'android')}
jdk = {}
for row in (w / 'jdk.txt').read_text().splitlines():
root, owner, target = row.split('|')
jdk[root, owner] = target
sites = [('base/Strings.java', 17, 'base.Preconditions.checkArgument', 'base.Preconditions'),
('base/Strings.java', 18, 'base.Preconditions.checkNotNull', 'base.Preconditions'),
('io/Files.java', 29, 'collect.ImmutableList', 'collect.ImmutableList')]
def record(mode, path, line, spec):
item, = (r for r in facts[mode]['imports'] if
(r['from'], r['line'], r['specifier']) == (path, line, 'com.google.common.' + spec))
return item
for mode, root in [('jre', 'guava/src'), ('android', 'android/guava/src')]:
for short, line, spec, owner in sites:
path = 'com/google/common/' + short
combined = record('both', root + '/' + path, line, spec)
single = record(mode, path, line, spec)
target = jdk[root, 'com.google.common.' + owner]
assert not combined['resolved'] and 'to' not in combined
assert single['resolved'] and root + '/' + single['to'] == target
print(f'{mode} {short}:{line} | combined: unresolved | single: {target}')
for mode, raw in facts.items():
print(mode, 'files', len(raw['files']), 'parse_errors', len(raw['parse_errors']))
PYA separate compiler-verified static-import case found that a field or method import could hide an unrelated same-name type from the analyzer. Draft #1231 corrects two authored missing targets. The pinned SLF4J and Guava scopes above are preservation controls: all their import/type-reference records remain unchanged, including existing parse-error counts. No occurrence of this trigger was found in the Guava audit.