englefly commented on code in PR #67824: URL: https://github.com/apache/doris/pull/67824#discussion_r4267572242
########## fe/fe-core/src/main/java/org/apache/doris/buildtools/IncrementalSourceMarker.java: ########## @@ -0,0 +1,588 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.doris.buildtools; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * Computes the set of sources which must be recompiled after a local edit and marks them for the + * compiler plugin, so that {@code mvn compile} stops rebuilding the whole fe-core module. + * + * <p>Background: {@code maven-compiler-plugin} is all-or-nothing by default (one stale source + * recompiles every source of the module), and its {@code useIncrementalCompilation=false} mode, + * which does compile only the stale sources, is unsafe on its own because it never recompiles the + * classes which <em>use</em> a changed class. + * + * <p>This tool closes that gap. For every scanned source file it caches, keyed by the content hash: + * <ul> + * <li>the identifiers the file mentions, which over-approximates the types it depends on,</li> + * <li>the type names the file declares.</li> + * </ul> + * A run then computes + * <pre> + * changed = files whose content changed, plus files whose set of declared names changed + * (a type was added, removed, renamed or moved) + * affected = changed, closed over "mentions a type name declared by an affected file" + * </pre> + * and updates the last modification time of every affected file which belongs to the main sources. + * The compiler plugin then compiles exactly that set. Because every file which mentions a changed + * type is touched, no class is left referring to a type which was changed or deleted underneath it; + * the set is a conservative superset, so the only cost of an imprecise name match is a few extra + * files being recompiled. + * + * <p>The compiler plugin still owns the compilation, its annotation processing and its classpath. + * This tool only decides which sources are stale. + */ +public class IncrementalSourceMarker { + /** Bump it whenever the cached facts change, then the index is rebuilt from scratch. */ + private static final int FORMAT_VERSION = 1; + + /** The java keywords which introduce a type name. {@code interface} also covers {@code @interface}. */ + private static final Set<String> DECLARATION_KEYWORDS = new HashSet<>( + Arrays.asList("class", "interface", "enum", "record")); + + /** + * The reserved java keywords, which can never be a type name. They are dropped from the mentioned + * identifiers to keep the index small and to avoid matching the keywords that would otherwise + * appear in every file. Contextual keywords such as {@code record} or {@code var} are deliberately + * not listed, they can be type names. + */ + private static final Set<String> RESERVED_KEYWORDS = new HashSet<>(Arrays.asList( + "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", + "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", + "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", + "new", "package", "private", "protected", "public", "return", "short", "static", "strictfp", + "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", "void", + "volatile", "while", "true", "false", "null", "_")); + + /** entry point, see the {@code fast-fe} profile of fe-core/pom.xml. */ + public static void main(String[] args) throws Exception { + long startNanos = System.nanoTime(); + Map<String, String> options = parseOptions(args); + List<File> sourceRoots = splitPaths(requireOption(options, "sources")); + File touchRoot = new File(requireOption(options, "touch-root")).getCanonicalFile(); + File cacheFile = new File(requireOption(options, "cache")); + File classesDir = options.containsKey("classes") + ? new File(options.get("classes")).getCanonicalFile() : null; + + List<File> sourceFiles = findJavaFiles(sourceRoots); + SourceIndex index = SourceIndex.load(cacheFile); + + Map<String, Facts> factsByPath = new LinkedHashMap<>(); + List<Facts> derivedSources = new ArrayList<>(); + Map<String, List<String>> declaredBy = new TreeMap<>(); + Map<String, List<String>> referrersBy = new TreeMap<>(); + Set<String> changed = new TreeSet<>(); + int scanned = 0; + for (File sourceFile : sourceFiles) { + String path = sourceFile.getAbsolutePath(); + byte[] contentHash = contentHash(sourceFile); + Facts facts = index.get(path, contentHash); + if (facts == null) { + facts = scan(sourceFile, contentHash); + index.put(path, facts); + changed.add(path); + scanned++; + } + factsByPath.put(path, facts); + if (!isUnder(sourceFile, touchRoot)) { + derivedSources.add(facts); + } + for (String declaredName : facts.declaredNames) { + declaredBy.computeIfAbsent(declaredName, name -> new ArrayList<>()).add(path); + } + for (String identifier : facts.identifiers) { + referrersBy.computeIfAbsent(identifier, name -> new ArrayList<>()).add(path); + } + } + + // A file which disappeared, or a type which is not declared by the same files as before + // (added, removed, renamed, moved to another package), invalidates the files which mention + // that name exactly like a content change does. A removed file itself has nothing left to + // compile, only its classes have to go away, and its type names are already covered by the + // names whose declaring files changed. + Set<String> affected = new TreeSet<>(); + for (String staleName : index.replaceDeclaredNames(declaredBy)) { + addReferrers(affected, referrersBy, staleName); + } + for (String removedPath : index.removeMissing(sourceFiles)) { + removeClasses(new File(removedPath), touchRoot, classesDir); + } + affected.addAll(changed); + + // Propagate one hop only: the files which mention a type declared by a changed file. A file Review Comment: 已确认并修复,谢谢这个反例。我按你的写法复现了:`A.VALUE` 从 2 改成 3 之后只标记了 A、B,`C.class` 里仍然是内联进去的旧值 `2`(`javap` 可见 `iconst_2`)。 **根因**:我原来的传播只沿"引用"方向做一跳,忽略了 Java 的一个基本性质 —— 子类的有效 API 包含从父类继承的成员。所以"引用写在子类型上"(`C` 用 `B.VALUE`)这种依赖,按名字匹配是看不到的。 **修法**(没有退化成全量传递闭包,那会让一次叶子编辑扩散到整个模块): - scanner 额外记录每个文件的 `extends` / `implements` 子句,构建 `父类型 → 子类型` 映射; - 传播种子从"被改类型本身"扩展为"被改类型的**传递子类型**闭包",再对这些名字做一次引用展开。 这样做的理由是:未编辑的文件被重编后自身声明不变,只有"继承来的有效 API"会变,所以需要传播的方向是**子类型方向**,而不是引用方向。 你建议的 ABI 指纹(用编译产物判断传播何时可以停止)我没有采用:那需要解析 `.class`,而且 marker 跑在编译之前,只能用一轮延迟来收敛。目前的做法是沿继承边界精确扩展的保守超集。 **验证**:叶子类编辑的标记集合不变(探针文件仍是 `129 marked` → `Compiling 146 source files`),只有改基类才会扩散。新增自测用例(在修复前必失败): ``` editing a supertype marks the files which name its subtype -> demo/Base.java demo/Holder.java demo/Uses.java ``` commit: e64b106a631 ########## fe/fe-core/src/main/java/org/apache/doris/buildtools/IncrementalSourceMarker.java: ########## @@ -0,0 +1,588 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.doris.buildtools; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * Computes the set of sources which must be recompiled after a local edit and marks them for the + * compiler plugin, so that {@code mvn compile} stops rebuilding the whole fe-core module. + * + * <p>Background: {@code maven-compiler-plugin} is all-or-nothing by default (one stale source + * recompiles every source of the module), and its {@code useIncrementalCompilation=false} mode, + * which does compile only the stale sources, is unsafe on its own because it never recompiles the + * classes which <em>use</em> a changed class. + * + * <p>This tool closes that gap. For every scanned source file it caches, keyed by the content hash: + * <ul> + * <li>the identifiers the file mentions, which over-approximates the types it depends on,</li> + * <li>the type names the file declares.</li> + * </ul> + * A run then computes + * <pre> + * changed = files whose content changed, plus files whose set of declared names changed + * (a type was added, removed, renamed or moved) + * affected = changed, closed over "mentions a type name declared by an affected file" + * </pre> + * and updates the last modification time of every affected file which belongs to the main sources. + * The compiler plugin then compiles exactly that set. Because every file which mentions a changed + * type is touched, no class is left referring to a type which was changed or deleted underneath it; + * the set is a conservative superset, so the only cost of an imprecise name match is a few extra + * files being recompiled. + * + * <p>The compiler plugin still owns the compilation, its annotation processing and its classpath. + * This tool only decides which sources are stale. + */ +public class IncrementalSourceMarker { + /** Bump it whenever the cached facts change, then the index is rebuilt from scratch. */ + private static final int FORMAT_VERSION = 1; + + /** The java keywords which introduce a type name. {@code interface} also covers {@code @interface}. */ + private static final Set<String> DECLARATION_KEYWORDS = new HashSet<>( + Arrays.asList("class", "interface", "enum", "record")); + + /** + * The reserved java keywords, which can never be a type name. They are dropped from the mentioned + * identifiers to keep the index small and to avoid matching the keywords that would otherwise + * appear in every file. Contextual keywords such as {@code record} or {@code var} are deliberately + * not listed, they can be type names. + */ + private static final Set<String> RESERVED_KEYWORDS = new HashSet<>(Arrays.asList( + "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", + "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", + "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", + "new", "package", "private", "protected", "public", "return", "short", "static", "strictfp", + "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", "void", + "volatile", "while", "true", "false", "null", "_")); + + /** entry point, see the {@code fast-fe} profile of fe-core/pom.xml. */ + public static void main(String[] args) throws Exception { + long startNanos = System.nanoTime(); + Map<String, String> options = parseOptions(args); + List<File> sourceRoots = splitPaths(requireOption(options, "sources")); + File touchRoot = new File(requireOption(options, "touch-root")).getCanonicalFile(); + File cacheFile = new File(requireOption(options, "cache")); + File classesDir = options.containsKey("classes") + ? new File(options.get("classes")).getCanonicalFile() : null; + + List<File> sourceFiles = findJavaFiles(sourceRoots); + SourceIndex index = SourceIndex.load(cacheFile); + + Map<String, Facts> factsByPath = new LinkedHashMap<>(); + List<Facts> derivedSources = new ArrayList<>(); + Map<String, List<String>> declaredBy = new TreeMap<>(); + Map<String, List<String>> referrersBy = new TreeMap<>(); + Set<String> changed = new TreeSet<>(); + int scanned = 0; + for (File sourceFile : sourceFiles) { + String path = sourceFile.getAbsolutePath(); + byte[] contentHash = contentHash(sourceFile); + Facts facts = index.get(path, contentHash); + if (facts == null) { + facts = scan(sourceFile, contentHash); + index.put(path, facts); + changed.add(path); + scanned++; + } + factsByPath.put(path, facts); + if (!isUnder(sourceFile, touchRoot)) { + derivedSources.add(facts); + } + for (String declaredName : facts.declaredNames) { + declaredBy.computeIfAbsent(declaredName, name -> new ArrayList<>()).add(path); + } + for (String identifier : facts.identifiers) { + referrersBy.computeIfAbsent(identifier, name -> new ArrayList<>()).add(path); + } + } + + // A file which disappeared, or a type which is not declared by the same files as before + // (added, removed, renamed, moved to another package), invalidates the files which mention + // that name exactly like a content change does. A removed file itself has nothing left to + // compile, only its classes have to go away, and its type names are already covered by the + // names whose declaring files changed. + Set<String> affected = new TreeSet<>(); + for (String staleName : index.replaceDeclaredNames(declaredBy)) { + addReferrers(affected, referrersBy, staleName); + } + for (String removedPath : index.removeMissing(sourceFiles)) { + removeClasses(new File(removedPath), touchRoot, classesDir); + } + affected.addAll(changed); + + // Propagate one hop only: the files which mention a type declared by a changed file. A file + // which is recompiled without being edited keeps its own api, so there is no reason to go + // further, and a transitive closure would quickly reach the whole module. + // + // The exception is a source derived from another one, such as the Immutables output of an + // annotated class: it is regenerated when its input changes, so its api may change too, and + // the files which use it need a recompilation as well. + for (String path : changed) { + Facts facts = factsByPath.get(path); + if (facts == null) { + continue; + } + for (String declaredName : facts.declaredNames) { + addReferrers(affected, referrersBy, declaredName); + for (Facts derived : derivedSources) { + if (derived.identifiers.contains(declaredName)) { + for (String derivedName : derived.declaredNames) { + addReferrers(affected, referrersBy, derivedName); + } + } + } + } + } + + int marked = 0; + FileTime now = FileTime.fromMillis(System.currentTimeMillis()); + for (String path : affected) { + File file = new File(path); + if (isUnder(file, touchRoot)) { + Files.setLastModifiedTime(file.toPath(), now); + marked++; + } + } + + index.save(cacheFile); + long elapsedMillis = (System.nanoTime() - startNanos) / 1_000_000; + System.out.println("[incremental-sources] " + sourceFiles.size() + " sources, " + scanned + + " scanned, " + changed.size() + " changed, " + marked + " marked for recompilation in " + + elapsedMillis + " ms"); + } + + private static void addReferrers(Set<String> affected, Map<String, List<String>> referrersBy, String name) { + List<String> referrers = referrersBy.get(name); + if (referrers != null) { + affected.addAll(referrers); + } + } + + /** delete the classes of a source file which does not exist anymore. */ + private static void removeClasses(File removedSource, File touchRoot, File classesDir) throws IOException { + if (classesDir == null || !isUnder(removedSource, touchRoot)) { + return; + } + String relative = touchRoot.toPath().relativize(removedSource.getCanonicalFile().toPath()).toString(); + String prefix = relative.substring(0, relative.length() - ".java".length()); + File parent = new File(classesDir, prefix).getParentFile(); + if (parent == null || !parent.isDirectory()) { + return; + } + String simpleName = new File(prefix).getName(); + File[] candidates = parent.listFiles((dir, name) -> name.endsWith(".class") + && (name.equals(simpleName + ".class") || name.startsWith(simpleName + "$"))); + if (candidates != null) { + for (File candidate : candidates) { + Files.deleteIfExists(candidate.toPath()); + } + } + } + + private static boolean isUnder(File file, File directory) { Review Comment: 已修复,谢谢指出。这个在 Linux 上很难发现,但在 macOS 上确实是必现的。 **根因确认**:`--touch-root` 走的是 `getCanonicalFile()`,而扫描出来的文件和索引 key 用的是 `getAbsolutePath()`。路径里含 symlink 时两者不同(macOS 上 `mktemp` 返回 `/var/folders/...`,canonical 形式是 `/private/var/folders/...`),于是 `isUnder()`(字符串前缀比较)对每个源文件都返回 false —— 不会 touch 任何文件,删文件的 class 也不会清理。你观察到的那 4 个失败用例正是这个原因。 **修法**: - 所有 root 只规范化一次(`toRealPath()`),扫描出来的路径全部由这些 canonical root 派生,索引 key 使用同一套路径; - 包含判断改为 `Path.startsWith`,顺带修掉前缀误判(`/a/bc` 不再被认成 root `/a/b` 下的文件); - 索引 `FORMAT_VERSION` 从 1 提到 2,用旧 key 写出来的索引直接作废,而不是被信任。 **验证**:自测里用一个真实的 symlink 源根复现(`$WORK/link -> $WORK/real`),修复前 0 个文件被标记、修复后正确标记。新增用例: ``` a symlinked source root still marks the referrers -> demo/Host.java demo/User.java ``` commit: e64b106a631 ########## fe/fe-core/src/main/java/org/apache/doris/buildtools/IncrementalSourceMarker.java: ########## @@ -0,0 +1,588 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.doris.buildtools; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * Computes the set of sources which must be recompiled after a local edit and marks them for the + * compiler plugin, so that {@code mvn compile} stops rebuilding the whole fe-core module. + * + * <p>Background: {@code maven-compiler-plugin} is all-or-nothing by default (one stale source + * recompiles every source of the module), and its {@code useIncrementalCompilation=false} mode, + * which does compile only the stale sources, is unsafe on its own because it never recompiles the + * classes which <em>use</em> a changed class. + * + * <p>This tool closes that gap. For every scanned source file it caches, keyed by the content hash: + * <ul> + * <li>the identifiers the file mentions, which over-approximates the types it depends on,</li> + * <li>the type names the file declares.</li> + * </ul> + * A run then computes + * <pre> + * changed = files whose content changed, plus files whose set of declared names changed + * (a type was added, removed, renamed or moved) + * affected = changed, closed over "mentions a type name declared by an affected file" + * </pre> + * and updates the last modification time of every affected file which belongs to the main sources. + * The compiler plugin then compiles exactly that set. Because every file which mentions a changed + * type is touched, no class is left referring to a type which was changed or deleted underneath it; + * the set is a conservative superset, so the only cost of an imprecise name match is a few extra + * files being recompiled. + * + * <p>The compiler plugin still owns the compilation, its annotation processing and its classpath. + * This tool only decides which sources are stale. + */ +public class IncrementalSourceMarker { + /** Bump it whenever the cached facts change, then the index is rebuilt from scratch. */ + private static final int FORMAT_VERSION = 1; + + /** The java keywords which introduce a type name. {@code interface} also covers {@code @interface}. */ + private static final Set<String> DECLARATION_KEYWORDS = new HashSet<>( + Arrays.asList("class", "interface", "enum", "record")); + + /** + * The reserved java keywords, which can never be a type name. They are dropped from the mentioned + * identifiers to keep the index small and to avoid matching the keywords that would otherwise + * appear in every file. Contextual keywords such as {@code record} or {@code var} are deliberately + * not listed, they can be type names. + */ + private static final Set<String> RESERVED_KEYWORDS = new HashSet<>(Arrays.asList( + "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", + "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", + "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", + "new", "package", "private", "protected", "public", "return", "short", "static", "strictfp", + "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", "void", + "volatile", "while", "true", "false", "null", "_")); + + /** entry point, see the {@code fast-fe} profile of fe-core/pom.xml. */ + public static void main(String[] args) throws Exception { + long startNanos = System.nanoTime(); + Map<String, String> options = parseOptions(args); + List<File> sourceRoots = splitPaths(requireOption(options, "sources")); + File touchRoot = new File(requireOption(options, "touch-root")).getCanonicalFile(); + File cacheFile = new File(requireOption(options, "cache")); + File classesDir = options.containsKey("classes") + ? new File(options.get("classes")).getCanonicalFile() : null; + + List<File> sourceFiles = findJavaFiles(sourceRoots); + SourceIndex index = SourceIndex.load(cacheFile); + + Map<String, Facts> factsByPath = new LinkedHashMap<>(); + List<Facts> derivedSources = new ArrayList<>(); + Map<String, List<String>> declaredBy = new TreeMap<>(); + Map<String, List<String>> referrersBy = new TreeMap<>(); + Set<String> changed = new TreeSet<>(); + int scanned = 0; + for (File sourceFile : sourceFiles) { + String path = sourceFile.getAbsolutePath(); + byte[] contentHash = contentHash(sourceFile); + Facts facts = index.get(path, contentHash); + if (facts == null) { + facts = scan(sourceFile, contentHash); + index.put(path, facts); + changed.add(path); + scanned++; + } + factsByPath.put(path, facts); + if (!isUnder(sourceFile, touchRoot)) { + derivedSources.add(facts); + } + for (String declaredName : facts.declaredNames) { + declaredBy.computeIfAbsent(declaredName, name -> new ArrayList<>()).add(path); + } + for (String identifier : facts.identifiers) { + referrersBy.computeIfAbsent(identifier, name -> new ArrayList<>()).add(path); + } + } + + // A file which disappeared, or a type which is not declared by the same files as before + // (added, removed, renamed, moved to another package), invalidates the files which mention + // that name exactly like a content change does. A removed file itself has nothing left to + // compile, only its classes have to go away, and its type names are already covered by the + // names whose declaring files changed. + Set<String> affected = new TreeSet<>(); + for (String staleName : index.replaceDeclaredNames(declaredBy)) { + addReferrers(affected, referrersBy, staleName); + } + for (String removedPath : index.removeMissing(sourceFiles)) { + removeClasses(new File(removedPath), touchRoot, classesDir); + } + affected.addAll(changed); + + // Propagate one hop only: the files which mention a type declared by a changed file. A file + // which is recompiled without being edited keeps its own api, so there is no reason to go + // further, and a transitive closure would quickly reach the whole module. + // + // The exception is a source derived from another one, such as the Immutables output of an + // annotated class: it is regenerated when its input changes, so its api may change too, and + // the files which use it need a recompilation as well. + for (String path : changed) { + Facts facts = factsByPath.get(path); + if (facts == null) { + continue; + } + for (String declaredName : facts.declaredNames) { + addReferrers(affected, referrersBy, declaredName); + for (Facts derived : derivedSources) { + if (derived.identifiers.contains(declaredName)) { + for (String derivedName : derived.declaredNames) { + addReferrers(affected, referrersBy, derivedName); + } + } + } + } + } + + int marked = 0; + FileTime now = FileTime.fromMillis(System.currentTimeMillis()); + for (String path : affected) { + File file = new File(path); + if (isUnder(file, touchRoot)) { + Files.setLastModifiedTime(file.toPath(), now); + marked++; + } + } + + index.save(cacheFile); + long elapsedMillis = (System.nanoTime() - startNanos) / 1_000_000; + System.out.println("[incremental-sources] " + sourceFiles.size() + " sources, " + scanned + + " scanned, " + changed.size() + " changed, " + marked + " marked for recompilation in " + + elapsedMillis + " ms"); + } + + private static void addReferrers(Set<String> affected, Map<String, List<String>> referrersBy, String name) { + List<String> referrers = referrersBy.get(name); + if (referrers != null) { + affected.addAll(referrers); + } + } + + /** delete the classes of a source file which does not exist anymore. */ + private static void removeClasses(File removedSource, File touchRoot, File classesDir) throws IOException { + if (classesDir == null || !isUnder(removedSource, touchRoot)) { + return; + } + String relative = touchRoot.toPath().relativize(removedSource.getCanonicalFile().toPath()).toString(); + String prefix = relative.substring(0, relative.length() - ".java".length()); + File parent = new File(classesDir, prefix).getParentFile(); + if (parent == null || !parent.isDirectory()) { + return; + } + String simpleName = new File(prefix).getName(); Review Comment: 已修复,谢谢。我复现了 `Extra.class` 残留。 **根因**:原来的清理是按源文件名去猜 class 名(`<File>.class` / `<File>$*.class`),但 Java 允许一个 `.java` 声明多个顶层类型,只有 public 的那个必须与文件名同名,包私有的 `class Extra` 就会漏掉。 **修法**: - scanner 按花括号深度记录每个文件的**顶层声明名**(嵌套类型仍然由 `<Outer>$...` 通配覆盖,所以不需要单独记录); - `removeMissing` 改为返回 `path → Facts`,清理时对**每个顶层类型**删除 `<Type>.class` 和 `<Type>$...class`; - 只删该文件声明过的类型,无关的类不会被动到(自测里保留了 `Removedish.class` 作为反例)。 **验证**:新增用例(修复前必失败),同时覆盖"多顶层类型 + 嵌套类 + 无关类保留": ``` removing a source deletes every class it declared, and no other -> Removed.class=gone Removed$Inner.class=gone Extra.class=gone Removedish.class=kept ``` commit: e64b106a631 -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
