feat(java): Phase 1.5 — tree-sitter Java frontend, package-aware mv, e2e fixtures

This commit is contained in:
loki5512344 2026-09-13 17:29:07 +02:00
parent 55670030c3
commit 603f916ee9
Signed by: boba
GPG key ID: 253067914055423B
25 changed files with 1288 additions and 366 deletions

View file

@ -4,20 +4,25 @@
//!
//! - A [`Language`] parses one source file into [`ImportRecord`]s: every
//! *static-ish* module reference (TS `import`/`export from`/`require`/
//! dynamic `import()`).
//! - [`crate::core::parser_support::resolve`] turns a specifier into a
//! project-relative file path using a resolver aware of the indexed file
//! set. Non-project (package/bare) specifiers resolve to `None`.
//! dynamic `import()`; Java single-type and static-member `import`s —
//! on-demand `pkg.*` imports are deliberately not extracted).
//! - Java files additionally expose their [`PackageDecl`] via
//! [`Language::extract_package`]; resolution of Java specifiers goes
//! through [`java::JavaClassIndex`] instead of [`resolve`] (TS relative
//! specifiers). External Java imports (jdk, third-party) simply never
//! appear in the class index.
//! - Rewrites must touch **only the specifier string**, never the rest of
//! the statement (KISS + no formatter dependency): that is why
//! [`ImportRecord::span`] is a byte range into the original source.
use std::ops::Range;
use std::path::Path;
pub mod java;
pub mod resolve;
pub mod ts;
/// Source languages `jmove` understands (Phase 1: TypeScript/JavaScript).
/// Source languages `jmove` understands (TS/JS in Phase 1, Java in Phase 1.5).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SourceLanguage {
/// `.ts` (non-TSX) sources.
@ -26,6 +31,8 @@ pub enum SourceLanguage {
Tsx,
/// Plain `.js` / `.mjs` / `.cjs` sources.
JavaScript,
/// `.java` sources.
Java,
}
impl SourceLanguage {
@ -36,6 +43,7 @@ impl SourceLanguage {
"ts" | "mts" | "cts" => Some(Self::TypeScript),
"tsx" | "jsx" => Some(Self::Tsx),
"js" | "mjs" | "cjs" => Some(Self::JavaScript),
"java" => Some(Self::Java),
_ => None,
}
}
@ -56,16 +64,27 @@ impl SourceLanguage {
/// One module reference found in a source file.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportRecord {
/// Raw specifier text as written, e.g. `"../utils/format"`.
/// Raw specifier text as written, e.g. `"../utils/format"` (TS) or
/// `"com.example.utils.Parser"` (Java).
pub specifier: String,
/// Byte range of the *specifier string contents* (inside the quotes,
/// without the quote characters) in the parsed file. The rewriter
/// replaces exactly this span and nothing else.
pub span: std::ops::Range<usize>,
pub span: Range<usize>,
/// `true` for dynamic `import("...")` / `require("...")` occurrences.
pub is_dynamic: bool,
}
/// A Java `package` declaration: dotted name plus the byte span of the name
/// (quotes have no meaning here — the span covers `com.example.utils`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackageDecl {
/// Declared package name, e.g. `"com.example.utils"`.
pub name: String,
/// Byte range of the package name in the parsed file.
pub span: Range<usize>,
}
/// A language frontend that extracts imports from source text.
pub trait Language: Send + Sync {
/// The language this frontend handles.
@ -74,10 +93,21 @@ pub trait Language: Send + Sync {
/// Extract all import records from `source` in byte-offset order.
/// Parse errors must not be fatal: return what was understood.
fn extract_imports(&self, source: &str) -> Vec<ImportRecord>;
/// Extract the `package` declaration, if this language has one and the
/// file declares it. Default: no package concept (TS/JS).
fn extract_package(&self, _source: &str) -> Option<PackageDecl> {
None
}
}
/// Build the default frontend for `lang`.
#[must_use]
pub fn frontend_for(lang: SourceLanguage) -> Box<dyn Language> {
Box::new(ts::TreeSitterTs::new(lang))
match lang {
SourceLanguage::Java => Box::new(java::TreeSitterJava::new()),
SourceLanguage::TypeScript | SourceLanguage::Tsx | SourceLanguage::JavaScript => {
Box::new(ts::TreeSitterTs::new(lang))
}
}
}