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]

Reply via email to