diff --git a/docs/development/platform-parity-matrix.csv b/docs/development/platform-parity-matrix.csv index 241d81460..d2d55ede2 100644 --- a/docs/development/platform-parity-matrix.csv +++ b/docs/development/platform-parity-matrix.csv @@ -58,7 +58,7 @@ lsp-lifecycle,Java,语言服务,LSP/JDTLS 启动与工作区准备,已实现,待 lsp-completion-hover,Java,语言服务,补全与 Hover,已实现,待验证,已实现,待验证,Language Tooling,在相同 Java fixture 中验证补全、Hover、排序、超时和空结果。,,macos/Sources/Lithe/Core/Language; shared/fixtures/lsp,windows/tauri/src/features; shared/fixtures/lsp lsp-diagnostics,Java,语言服务,诊断与编译错误定位,已实现,待验证,已实现,待验证,Language Tooling,制造语法和类型错误,比较诊断等级、消息、行列号和清理行为。,,macos/Sources/Lithe/Core/Language; macos/Sources/Lithe/Services/Diagnostics,windows/tauri/src/features/diagnostics; shared/fixtures/lsp lsp-navigation-edits,Java,语言服务,跳转、引用、语义标记与编辑,已实现,待验证,已实现,待验证,Language Tooling,验证定义、引用、语义标记和 UTF-16 文本编辑的坐标转换。,,macos/Sources/Lithe/Core/Language; shared/contracts/rust-core-api.md,windows/tauri/src/features; shared/fixtures/lsp -run-discovery,运行与调试,运行配置,入口点与运行配置发现,已实现,待验证,已实现,待验证,Run,验证 Spring Boot、Java、Maven、Gradle、npm、Cargo、Go、Python 和 Docker Compose 入口识别;多模块项目把服务工作目录改成模块目录后,服务仍留在列表中并以该目录启动;填写不存在的目录或 ${workspaceFolder} 等变量时,保存被拒绝并提示原因;在 Windows 确认重新扫描服务使用双箭头刷新图标。,,macos/Sources/Lithe/Views/Run; shared/fixtures/run-configuration,windows/tauri/src/features/run; shared/fixtures/run-configuration +run-discovery,运行与调试,运行配置,入口点与运行配置发现,已实现,待验证,已实现,待验证,Run,验证 Spring Boot、Java、Maven、Gradle、npm、Cargo、Go、Python 和 Docker Compose 入口识别;框架 plugin 只在父 POM 声明时,子模块(含多级嵌套)仍识别为对应服务,而 声明保持不生效;多模块项目把服务工作目录改成模块目录后,服务仍留在列表中并以该目录启动;填写不存在的目录或 ${workspaceFolder} 等变量时,保存被拒绝并提示原因;在 Windows 确认重新扫描服务使用双箭头刷新图标。,,macos/Sources/Lithe/Views/Run; shared/fixtures/run-configuration,windows/tauri/src/features/run; rust/lithe-core/src/project/maven.rs; rust/lithe-core/src/execution/detectors/maven.rs; shared/fixtures/run-configuration run-save-toolchain,运行与调试,运行配置,保存前同步与工具链解析,已实现,待验证,已实现,待验证,Run,修改未保存文件后运行,验证同步、JDK/Maven 选择和版本不匹配诊断。,,macos/Sources/Lithe/Views/Run; macos/Sources/Lithe/Services/Java,windows/tauri/src/features/run; windows/tauri/src/features/maven run-java-test,运行与调试,运行配置,Java main 与测试运行,已实现,待验证,已实现,待验证,Run,运行 main、单测试和测试类,验证参数、输出、失败状态和终端策略。 Windows Run 运行输出使用 Geist Mono 显示 ====、==>、=>、!=、>=,确认禁用连字后逐字符显示且复制文本与原始输出一致;检查有输出与空态、ANSI 颜色、自动换行切换及横向滚动。,,macos/Sources/Lithe/Views/Run; shared/fixtures/debug,windows/tauri/src/features/run; shared/fixtures/debug; windows/tauri/src/features/run/components/run-pane.tsx; windows/tauri/src/features/run/components/run-output-text.tsx run-java-long-classpath,运行与调试,运行配置,超长 Java 类路径自动缩短,已实现,待验证,已实现,待验证,Run,使用包含大量依赖的 Java 项目运行 main,确认超长类路径自动写入参数文件、进程可启动且参数文件在退出后清理。 Windows 上分别用 JDK 8 和 JDK 17 运行同一个依赖很多、路径含空格和中文的项目 main:JDK 8 应在临时目录生成只含 META-INF/MANIFEST.MF 的 .classpath.jar 并以 -cp 引用,JDK 17 仍使用 @argfile;两者都能启动,进程退出或启动失败后临时文件被删除,安装目录与 JDK 目录不新增文件。 在慢速网络盘 classpath 准备期间确认工作台可交互;停止或重新运行后,旧准备结果不得启动进程,旧 executionId 的停止请求不得影响新执行。,,macos/Sources/Lithe/Platform/MacOS/RunConfiguration/MacJavaLaunchArgumentPreparer.swift; macos/Sources/LitheExecutionModule/Services/RunService.swift; rust/lithe-core/src/execution/launch_command.rs,windows/tauri/src-tauri/src/run/launch_arguments.rs; rust/lithe-core/src/execution/launch_command.rs; rust/lithe-core/src/execution/classpath_jar.rs; windows/tauri/src-tauri/src/run.rs diff --git a/docs/development/platform-parity-matrix.md b/docs/development/platform-parity-matrix.md index 4305bb633..330559afd 100644 --- a/docs/development/platform-parity-matrix.md +++ b/docs/development/platform-parity-matrix.md @@ -167,7 +167,7 @@ | 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 | | --- | --- | --- | --- | --- | --- | --- | -| 运行配置 | **入口点与运行配置发现**
run-discovery | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Run`、`shared/fixtures/run-configuration` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/run`、`shared/fixtures/run-configuration` | Run | 验证 Spring Boot、Java、Maven、Gradle、npm、Cargo、Go、Python 和 Docker Compose 入口识别;多模块项目把服务工作目录改成模块目录后,服务仍留在列表中并以该目录启动;填写不存在的目录或 ${workspaceFolder} 等变量时,保存被拒绝并提示原因;在 Windows 确认重新扫描服务使用双箭头刷新图标。 | | +| 运行配置 | **入口点与运行配置发现**
run-discovery | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Run`、`shared/fixtures/run-configuration` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/run`、`rust/lithe-core/src/project/maven.rs`、`rust/lithe-core/src/execution/detectors/maven.rs`、`shared/fixtures/run-configuration` | Run | 验证 Spring Boot、Java、Maven、Gradle、npm、Cargo、Go、Python 和 Docker Compose 入口识别;框架 plugin 只在父 POM 声明时,子模块(含多级嵌套)仍识别为对应服务,而 声明保持不生效;多模块项目把服务工作目录改成模块目录后,服务仍留在列表中并以该目录启动;填写不存在的目录或 ${workspaceFolder} 等变量时,保存被拒绝并提示原因;在 Windows 确认重新扫描服务使用双箭头刷新图标。 | | | 运行配置 | **保存前同步与工具链解析**
run-save-toolchain | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Run`、`macos/Sources/Lithe/Services/Java` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/run`、`windows/tauri/src/features/maven` | Run | 修改未保存文件后运行,验证同步、JDK/Maven 选择和版本不匹配诊断。 | | | 运行配置 | **Java main 与测试运行**
run-java-test | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Run`、`shared/fixtures/debug` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/run`、`shared/fixtures/debug`、`windows/tauri/src/features/run/components/run-pane.tsx`、`windows/tauri/src/features/run/components/run-output-text.tsx` | Run | 运行 main、单测试和测试类,验证参数、输出、失败状态和终端策略。 Windows Run 运行输出使用 Geist Mono 显示 ====、==>、=>、!=、>=,确认禁用连字后逐字符显示且复制文本与原始输出一致;检查有输出与空态、ANSI 颜色、自动换行切换及横向滚动。 | | | 运行配置 | **超长 Java 类路径自动缩短**
run-java-long-classpath | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Platform/MacOS/RunConfiguration/MacJavaLaunchArgumentPreparer.swift`、`macos/Sources/LitheExecutionModule/Services/RunService.swift`、`rust/lithe-core/src/execution/launch_command.rs` | ✅ 已实现
🔍 待验证
`windows/tauri/src-tauri/src/run/launch_arguments.rs`、`rust/lithe-core/src/execution/launch_command.rs`、`rust/lithe-core/src/execution/classpath_jar.rs`、`windows/tauri/src-tauri/src/run.rs` | Run | 使用包含大量依赖的 Java 项目运行 main,确认超长类路径自动写入参数文件、进程可启动且参数文件在退出后清理。 Windows 上分别用 JDK 8 和 JDK 17 运行同一个依赖很多、路径含空格和中文的项目 main:JDK 8 应在临时目录生成只含 META-INF/MANIFEST.MF 的 .classpath.jar 并以 -cp 引用,JDK 17 仍使用 @argfile;两者都能启动,进程退出或启动失败后临时文件被删除,安装目录与 JDK 目录不新增文件。 在慢速网络盘 classpath 准备期间确认工作台可交互;停止或重新运行后,旧准备结果不得启动进程,旧 executionId 的停止请求不得影响新执行。 | | diff --git a/rust/lithe-core/src/project/maven.rs b/rust/lithe-core/src/project/maven.rs index f9442e7e1..10ad76750 100644 --- a/rust/lithe-core/src/project/maven.rs +++ b/rust/lithe-core/src/project/maven.rs @@ -672,6 +672,10 @@ struct Descriptor { group_id: Option, artifact_id: Option, version: Option, + /// `artifactId` of ``, recorded so a module can be matched back to + /// the reactor entry it inherits from. `relativePath` is not needed: the + /// reactor tree already places every parent above its children. + parent_artifact_id: Option, packaging: String, build_directory: Option, /// `` values configured for Surefire or Failsafe, raw. @@ -738,7 +742,7 @@ pub fn declared_modules(root: &Path) -> Result, CoreError> { }; let mut modules = Vec::new(); let mut visited = vec![root.to_path_buf()]; - collect_modules(root, root, root_descriptor, &mut modules, &mut visited); + collect_modules(root, root, root_descriptor, &mut modules, &mut visited, &[]); Ok(modules) } @@ -752,6 +756,7 @@ fn collect_modules( current: Descriptor, modules: &mut Vec, visited: &mut Vec, + inherited_plugins: &[String], ) { let relative_path = directory .strip_prefix(root) @@ -760,6 +765,20 @@ fn collect_modules( .filter(|value| !value.is_empty()) .unwrap_or_else(|| ".".to_string()); let current_source_roots = source_roots(Some(¤t)); + // Maven merges a parent's `` into every child, so a boot + // plugin declared on an aggregator reaches the modules below it. Only the + // ancestor plugins already collected on the way down are merged: a child + // redeclaring the same `artifactId` is already present, which is the same + // answer either way because callers only ask whether a plugin is present. + // `` never reaches this set because `descriptor` collects + // `project/build/plugins` only, so a managed-but-unapplied plugin stays + // inert. + let mut effective_plugins = inherited_plugins.to_vec(); + for plugin in ¤t.plugins { + if !effective_plugins.contains(plugin) { + effective_plugins.push(plugin.clone()); + } + } modules.push(DeclaredModule { relative_path, artifact_id: current.artifact_id.unwrap_or_else(|| { @@ -770,7 +789,7 @@ fn collect_modules( .to_string() }), packaging: current.packaging, - plugins: current.plugins, + plugins: effective_plugins.clone(), source_roots: current_source_roots, }); for raw_path in ¤t.module_paths { @@ -785,7 +804,14 @@ fn collect_modules( continue; }; visited.push(child.clone()); - collect_modules(root, &child, child_descriptor, modules, visited); + collect_modules( + root, + &child, + child_descriptor, + modules, + visited, + &effective_plugins, + ); } } @@ -1904,6 +1930,12 @@ fn descriptor(path: &Path) -> Result, CoreError> { } } "project/artifactId" => value.artifact_id = non_empty(text.clone()), + // `` is what identifies the reactor entry a + // module inherits from; without it a parent declared only in + // `` cannot be reached from the child. + "project/parent/artifactId" => { + value.parent_artifact_id = non_empty(text.clone()) + } "project/version" | "project/parent/version" => { if value.version.is_none() || path == "project/version" { value.version = non_empty(text.clone()); diff --git a/rust/lithe-core/src/tests/detectors.rs b/rust/lithe-core/src/tests/detectors.rs index e6b9eea19..377f7d51b 100644 --- a/rust/lithe-core/src/tests/detectors.rs +++ b/rust/lithe-core/src/tests/detectors.rs @@ -1673,3 +1673,95 @@ fn java_entries_are_exactly_the_classes_jdt_reports() { fs::remove_dir_all(root).unwrap(); } + +/// Maven merges a parent's `` into every child, so a boot +/// plugin an aggregator declares reaches the modules below it. Without that the +/// child is emitted as a plain application and never offered as a service. +#[test] +fn maven_detector_inherits_the_boot_plugin_from_the_aggregator() { + let root = temporary_root("detect-maven-inherited-boot-plugin"); + fs::create_dir_all(root.join("service-web")).unwrap(); + fs::write( + root.join("pom.xml"), + "parentpomservice-webspring-boot-maven-plugin", + ) + .unwrap(); + fs::write( + root.join("service-web/pom.xml"), + "parentservice-web", + ) + .unwrap(); + + let ids = generated_configurations(&root) + .into_iter() + .filter(|item| item["provider"] == "spring-boot.maven") + .map(|item| item["id"].as_str().unwrap().to_string()) + .collect::>(); + + assert_eq!(ids, vec!["spring-boot.maven:service-web".to_string()]); + + fs::remove_dir_all(root).unwrap(); +} + +/// Inheritance crosses more than one level: a plugin declared on the reactor +/// root is what the grandchild actually inherits, and the intermediate +/// aggregator contributes nothing of its own. +#[test] +fn maven_detector_inherits_the_boot_plugin_across_several_levels() { + let root = temporary_root("detect-maven-inherited-boot-plugin-deep"); + fs::create_dir_all(root.join("team/service-web")).unwrap(); + fs::write( + root.join("pom.xml"), + "rootpomteamspring-boot-maven-plugin", + ) + .unwrap(); + fs::write( + root.join("team/pom.xml"), + "root../pom.xmlteampomservice-web", + ) + .unwrap(); + fs::write( + root.join("team/service-web/pom.xml"), + "team../pom.xmlservice-web", + ) + .unwrap(); + + let ids = generated_configurations(&root) + .into_iter() + .filter(|item| item["provider"] == "spring-boot.maven") + .map(|item| item["id"].as_str().unwrap().to_string()) + .collect::>(); + + assert_eq!(ids, vec!["spring-boot.maven:service-web".to_string()]); + + fs::remove_dir_all(root).unwrap(); +} + +/// `` pins a version for children without applying the plugin, +/// and inheriting a parent must not turn a managed plugin into a service. This is +/// the counterpart to the inheritance above: the same parent shape, but the +/// plugin stays inert. +#[test] +fn maven_detector_does_not_inherit_a_plugin_managed_parent() { + let root = temporary_root("detect-maven-inherited-plugin-management"); + fs::create_dir_all(root.join("service-web")).unwrap(); + fs::write( + root.join("pom.xml"), + "parentpomservice-webspring-boot-maven-plugin", + ) + .unwrap(); + fs::write( + root.join("service-web/pom.xml"), + "parentservice-web", + ) + .unwrap(); + + assert!( + !generated_configurations(&root) + .iter() + .any(|item| item["provider"] == "spring-boot.maven"), + "a managed plugin must stay inert even when a parent declares it" + ); + + fs::remove_dir_all(root).unwrap(); +} diff --git a/shared/platform-feature-matrix.json b/shared/platform-feature-matrix.json index ad707092a..5f9ec8b0c 100644 --- a/shared/platform-feature-matrix.json +++ b/shared/platform-feature-matrix.json @@ -1569,13 +1569,15 @@ "windows": { "evidence": [ "windows/tauri/src/features/run", + "rust/lithe-core/src/project/maven.rs", + "rust/lithe-core/src/execution/detectors/maven.rs", "shared/fixtures/run-configuration" ], "implementationStatus": "implemented", "verificationStatus": "pending" }, "owner": "Run", - "verification": "验证 Spring Boot、Java、Maven、Gradle、npm、Cargo、Go、Python 和 Docker Compose 入口识别;多模块项目把服务工作目录改成模块目录后,服务仍留在列表中并以该目录启动;填写不存在的目录或 ${workspaceFolder} 等变量时,保存被拒绝并提示原因;在 Windows 确认重新扫描服务使用双箭头刷新图标。" + "verification": "验证 Spring Boot、Java、Maven、Gradle、npm、Cargo、Go、Python 和 Docker Compose 入口识别;框架 plugin 只在父 POM 声明时,子模块(含多级嵌套)仍识别为对应服务,而 声明保持不生效;多模块项目把服务工作目录改成模块目录后,服务仍留在列表中并以该目录启动;填写不存在的目录或 ${workspaceFolder} 等变量时,保存被拒绝并提示原因;在 Windows 确认重新扫描服务使用双箭头刷新图标。" }, { "id": "run-save-toolchain",