fix(run): offer Run for classes whose superclass declares main (#1204)

The JVM run-target scanner only reads the file's own text, so a class like `class App extends AtomStarter {}`, whose superclass (in the project or a library JAR) declares `static main`, got no gutter Run, no Ctrl+Shift+R and no file-level Run, although the JVM launches it. Classes with an `extends` clause and no main of their own are now candidates: the superclass chain is walked through the language server's type hierarchy, each class supertype's source (file, decompiled or source-jar document) is checked for a static main, and the answer is cached per class and `extends` clause, so the gutter, Run from context, launch and debug agree. Interfaces are skipped (a static interface main is not inherited); negative answers are asked again when the server settles. `main(java.lang.String[] a)` is accepted as well.

Co-authored-by: ntbang0901 <[email protected]>
Цей коміт міститься в:
ntbang0901 authored and GitHub committed 2026-11-04 23:11:01 +08:00
1 джерело 8f77d7077c
коміт d032addb17
7 files changed
+431 -9

No files matched your search

+33 -1
Переглянути файл
@@ -11,7 +11,7 @@ import { useProject } from "@/platform/project";
import { applicationConfig, jvmModuleDir, jvmTestConfig } from "./application";
import { buildRows } from "./ConfigChooser";
import { jdwpLaunch } from "./javaDebug";
import { scanJvmTargets } from "./jvmTargets";
import { jvmMainCandidates, scanJvmTargets } from "./jvmTargets";
import { adoptTemporary, MAX_TEMPORARY, saveTemporary, testReportEvents, useRun } from "./store";
import { detectRunTargets, targetConfig } from "./targets";
@@ -436,3 +436,35 @@ describe("temporary configurations", () => {
expect(useRun.getState().temporary).toEqual([]);
});
});
describe("jvmMainCandidates", () => {
const names = (src: string) => jvmMainCandidates(src, "a/B.java").map((c) => c.className);
it("finds the class from the report, at its name", () => {
const src = "package demo;\n\n@SpringBootApplication\npublic class AiChatApplication extends AtomStarter {}\n";
expect(jvmMainCandidates(src, "a/B.java")).toEqual([
{ line: 3, col: 13, className: "demo.AiChatApplication", base: "AtomStarter", spring: true },
]);
});
it.each([
["static nested class", "class A { static class B extends Base {} }", ["A$B"]],
["inner (non-static) class", "class A { class B extends Base {} }", []],
["abstract class", "abstract class A extends Base {}", ["A"]],
["own main", "class A extends Base { public static void main(String[] a) {} }", []],
["implements only", "class A implements Runnable {}", []],
["interface extends", "interface A extends Base {}", []],
["enum and record", "enum E implements I {}\nrecord R() implements I {}", []],
["multi-line generic header", "class A<T extends Number>\n extends Base<T>\n{}", ["A"]],
["type bound only", "class A<T extends Number> {}", []],
["extends in comment and string", '// class A extends B\nclass C { String s = "class D extends E {"; }', []],
])("%s", (_, src, expected) => expect(names(src)).toEqual(expected));
it("accepts java.lang.String[] and flags instance main", () => {
const [fq] = scanJvmTargets("class A { public static void main(java.lang.String[] a) {} }", "A.java", false);
expect(fq).toMatchObject({ kind: "main", className: "A" });
const [inst] = scanJvmTargets("class A { void main() {} }", "A.java", false);
expect(inst).toMatchObject({ kind: "main", instance: true });
expect(fq?.instance).toBeUndefined();
});
});