use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::ApiSurface;
use crate::core::template_versions::{maven, toolchain};
use crate::{scaffold::parse_author, scaffold::scaffold_meta, scaffold::xml_escape};
use anyhow::Context as _;
use minijinja::context;
use std::path::{Path, PathBuf};
fn java_capsule_dependencies(config: &ResolvedCrateConfig) -> Vec<minijinja::Value> {
let mut deps: Vec<(String, String)> = config
.java
.as_ref()
.map(|c| {
c.capsule_types
.values()
.filter(|cap| !cap.package.is_empty())
.map(|cap| (cap.package.clone(), cap.package_version.clone()))
.collect()
})
.unwrap_or_default();
deps.sort();
deps.dedup();
deps.iter()
.map(|(coord, ver)| {
let (group_id, artifact_id) = coord.split_once(':').unwrap_or((coord.as_str(), ""));
context! {
group_id => group_id,
artifact_id => artifact_id,
version => ver,
}
})
.collect()
}
pub(crate) fn scaffold_java(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let meta = scaffold_meta(config);
let name = config.java_artifact_id();
let name = name.as_str();
let version = &api.version;
let repo_url = meta.configured_repository.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Java scaffold requires package metadata repository; set package_metadata.repository or scaffold.repository"
)
})?;
if meta.authors.is_empty() {
anyhow::bail!(
"Java scaffold requires package metadata authors; set package_metadata.authors or scaffold.authors"
);
}
let license = meta.license.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Java scaffold requires package metadata license; set package_metadata.license or scaffold.license"
)
})?;
let scm = scm_urls(repo_url);
let group_id = config.java_group_id();
let source_root = group_id.split('.').next().unwrap_or("dev");
let authors: Vec<minijinja::Value> = meta
.authors
.iter()
.map(|a| {
let (name, email) = parse_author(a);
context! {
name => xml_escape(name),
email => xml_escape(email),
}
})
.collect();
let license_url = match license {
"Elastic-2.0" => "https://www.elastic.co/licensing/elastic-license",
"MIT" => "https://opensource.org/licenses/MIT",
"Apache-2.0" => "https://www.apache.org/licenses/LICENSE-2.0",
_ => "",
};
let content = crate::scaffold::template_env::render(
"java_pom.xml.jinja",
context! {
group_id => group_id,
name => name,
version => version,
description => meta.description,
repository => repo_url,
license => license,
license_url => license_url,
authors => authors,
scm_connection => scm.connection,
scm_developer_connection => scm.developer_connection,
capsule_deps => java_capsule_dependencies(config),
source_root => source_root,
java_release => toolchain::JAVA_JVM_TARGET,
junit_version => maven::JUNIT,
maven_core_version => maven::MAVEN_CORE,
maven_compiler_plugin_version => maven::MAVEN_COMPILER_PLUGIN,
maven_surefire_plugin_version => maven::MAVEN_SUREFIRE_PLUGIN,
maven_checkstyle_plugin_version => maven::MAVEN_CHECKSTYLE_PLUGIN,
maven_source_plugin_version => maven::MAVEN_SOURCE_PLUGIN,
maven_javadoc_plugin_version => maven::MAVEN_JAVADOC_PLUGIN,
maven_gpg_plugin_version => maven::MAVEN_GPG_PLUGIN,
maven_clean_plugin_version => maven::MAVEN_CLEAN_PLUGIN,
maven_resources_plugin_version => maven::MAVEN_RESOURCES_PLUGIN,
maven_jar_plugin_version => maven::MAVEN_JAR_PLUGIN,
maven_install_plugin_version => maven::MAVEN_INSTALL_PLUGIN,
maven_deploy_plugin_version => maven::MAVEN_DEPLOY_PLUGIN,
maven_site_plugin_version => maven::MAVEN_SITE_PLUGIN,
central_publishing_plugin_version => maven::CENTRAL_PUBLISHING_PLUGIN,
versions_maven_plugin_version => maven::VERSIONS_MAVEN_PLUGIN,
maven_enforcer_plugin_version => maven::MAVEN_ENFORCER_PLUGIN,
jacoco_maven_plugin_version => maven::JACOCO_MAVEN_PLUGIN,
checkstyle_version => maven::CHECKSTYLE,
jspecify_version => maven::JSPECIFY,
jackson_version => maven::JACKSON,
assertj_version => maven::ASSERTJ,
},
);
let checkstyle_xml = r#"<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<!-- Checkstyle handles correctness checks only. Formatting is handled by poly. -->
<module name="Checker">
<property name="charset" value="UTF-8" />
<property name="severity" value="error" />
<property name="fileExtensions" value="java" />
<module name="SuppressionFilter">
<property name="file" value="checkstyle-suppressions.xml" />
<property name="optional" value="true" />
</module>
<module name="LineLength">
<!-- 200 accommodates the alef-emitted DefaultClient.java FFM call shims:
the codegen chains arena allocation, MemorySegment marshalling, and
error-result handling onto single lines that don't reflow cleanly.
Tests and hand-written code stay well below this; the limit only
gives the generator headroom. -->
<property name="max" value="200" />
<property name="ignorePattern" value="^package.*|^import.*|a]href|href|http://|https://|ftp://" />
</module>
<module name="TreeWalker">
<!-- Naming Conventions (relaxed for FFI snake_case from Rust) -->
<module name="ConstantName">
<property name="format" value="^([A-Z][A-Z0-9]*(_[A-Z0-9]+)*|[a-z][a-zA-Z0-9_]*)$" />
</module>
<module name="PackageName" />
<module name="TypeName" />
<!-- Modifier Checks -->
<module name="ModifierOrder" />
<module name="RedundantModifier" />
<!-- Imports -->
<module name="UnusedImports" />
<!-- Coding -->
<module name="EmptyStatement" />
<module name="EqualsHashCode" />
<module name="SimplifyBooleanExpression" />
<module name="SimplifyBooleanReturn" />
<!-- Size Violations -->
<module name="MethodLength">
<property name="max" value="150" />
</module>
<!-- Misc -->
<module name="ArrayTypeStyle" />
<module name="UpperEll" />
</module>
</module>
"#;
let checkstyle_properties = "";
let checkstyle_suppressions_xml = r#"<?xml version="1.0"?>
<!DOCTYPE suppressions PUBLIC
"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
"https://checkstyle.org/dtds/suppressions_1_2.dtd">
<suppressions>
<!-- FFI constants -->
<suppress checks="ConstantName" files=".*FFI\.java" />
<suppress checks="MagicNumber" files=".*FFI\.java" />
<!-- Allow star imports and magic numbers in test files -->
<suppress checks="AvoidStarImport" files=".*Test\.java" />
<suppress checks="MagicNumber" files=".*Test\.java" />
<suppress checks="MethodLength" files=".*Test\.java" />
</suppressions>
"#;
Ok(vec.*</ignoreVersion>
<ignoreVersion type="regex">(?i).*[.-]m\d+.*</ignoreVersion>
</ignoreVersions>
</ruleset>
"#
.to_string(),
generated_header: false,
},
])
}
const STALE_LINE_LENGTH_120: &str = "<property name=\"max\" value=\"120\"/>\n <property name=\"ignorePattern\" value=\"^package.*|^import.*|a]href|href|http://|https://|ftp://\"/>";
const STALE_LINE_LENGTH_140: &str = "<property name=\"max\" value=\"140\"/>\n <property name=\"ignorePattern\" value=\"^package.*|^import.*|a]href|href|http://|https://|ftp://\"/>";
const FIXED_LINE_LENGTH: &str = "<property name=\"max\" value=\"200\"/>\n <property name=\"ignorePattern\" value=\"^package.*|^import.*|a]href|href|http://|https://|ftp://\"/>";
pub(crate) fn migrate_java_checkstyle_line_length(base_dir: &Path, relative_path: &Path) -> anyhow::Result<bool> {
let path = base_dir.join(relative_path);
let Ok(existing) = std::fs::read_to_string(&path) else {
return Ok(false);
};
let stale = [STALE_LINE_LENGTH_120, STALE_LINE_LENGTH_140]
.into_iter()
.find(|candidate| existing.matches(candidate).count() == 1);
let Some(stale) = stale else {
return Ok(false);
};
let migrated = existing.replacen(stale, FIXED_LINE_LENGTH, 1);
let parent = path.parent().context("checkstyle.xml path has no parent directory")?;
let mut temporary = tempfile::NamedTempFile::new_in(parent)
.with_context(|| format!("failed to create temporary file in {}", parent.display()))?;
std::io::Write::write_all(&mut temporary, migrated.as_bytes())
.with_context(|| format!("failed to write temporary file for {}", path.display()))?;
temporary
.persist(&path)
.map_err(|error| error.error)
.with_context(|| format!("failed to replace {}", path.display()))?;
tracing::info!(
path = %path.display(),
"repaired pre-existing packages/java/checkstyle.xml: raised LineLength max to 200 \
to accommodate alef-emitted FFM call shims"
);
Ok(true)
}
struct ScmUrls {
connection: String,
developer_connection: String,
}
fn scm_urls(repository: &str) -> ScmUrls {
let normalized = repository.trim_end_matches(".git");
let without_scheme = normalized
.strip_prefix("https://")
.or_else(|| normalized.strip_prefix("http://"))
.unwrap_or(normalized);
let (host, path) = without_scheme.split_once('/').unwrap_or((without_scheme, ""));
let suffix = if path.is_empty() {
String::new()
} else {
format!("/{path}.git")
};
ScmUrls {
connection: format!("scm:git:git://{host}{suffix}"),
developer_connection: format!("scm:git:ssh://git@{host}{suffix}"),
}
}
#[cfg(test)]
mod migrate_tests {
use super::*;
fn checkstyle_with_line_length(max_property: &str) -> String {
format!(
"<?xml version=\"1.0\"?>\n<module name=\"Checker\">\n <module name=\"LineLength\">\n {max_property}\n <property name=\"ignorePattern\" value=\"^package.*|^import.*|a]href|href|http://|https://|ftp://\"/>\n </module>\n <module name=\"TreeWalker\">\n <module name=\"MethodLength\">\n <property name=\"max\" value=\"150\"/>\n </module>\n </module>\n</module>\n"
)
}
#[test]
fn should_raise_line_length_to_200_when_stale_at_120() {
let dir = tempfile::tempdir().expect("tempdir");
let pkg_dir = dir.path().join("packages/java");
std::fs::create_dir_all(&pkg_dir).expect("create packages/java");
let stale = checkstyle_with_line_length("<property name=\"max\" value=\"120\"/>");
std::fs::write(pkg_dir.join("checkstyle.xml"), &stale).expect("write stale checkstyle.xml");
let relative_path = Path::new("packages/java/checkstyle.xml");
let changed = migrate_java_checkstyle_line_length(dir.path(), relative_path).expect("must not error");
assert!(
changed,
"a checkstyle.xml stuck at the old LineLength ceiling must be reported as changed"
);
let on_disk = std::fs::read_to_string(pkg_dir.join("checkstyle.xml")).expect("read migrated file");
assert!(on_disk.contains("<property name=\"max\" value=\"200\"/>"));
assert!(!on_disk.contains("value=\"120\""));
assert!(
on_disk.contains("<property name=\"max\" value=\"150\"/>"),
"MethodLength's unrelated max property must survive untouched"
);
let changed_again = migrate_java_checkstyle_line_length(dir.path(), relative_path).expect("must not error");
assert!(
!changed_again,
"second pass over an already-migrated file must be a no-op"
);
}
#[test]
fn should_raise_line_length_to_200_when_stale_at_140() {
let dir = tempfile::tempdir().expect("tempdir");
let pkg_dir = dir.path().join("packages/java");
std::fs::create_dir_all(&pkg_dir).expect("create packages/java");
let stale = checkstyle_with_line_length("<property name=\"max\" value=\"140\"/>");
std::fs::write(pkg_dir.join("checkstyle.xml"), &stale).expect("write stale checkstyle.xml");
let relative_path = Path::new("packages/java/checkstyle.xml");
let changed = migrate_java_checkstyle_line_length(dir.path(), relative_path).expect("must not error");
assert!(changed);
let on_disk = std::fs::read_to_string(pkg_dir.join("checkstyle.xml")).expect("read migrated file");
assert!(on_disk.contains("<property name=\"max\" value=\"200\"/>"));
assert!(!on_disk.contains("value=\"140\""));
}
#[test]
fn should_not_touch_a_checkstyle_with_a_customised_line_length() {
let dir = tempfile::tempdir().expect("tempdir");
let pkg_dir = dir.path().join("packages/java");
std::fs::create_dir_all(&pkg_dir).expect("create packages/java");
let customised = checkstyle_with_line_length("<property name=\"max\" value=\"100\"/>");
std::fs::write(pkg_dir.join("checkstyle.xml"), &customised).expect("write customised checkstyle.xml");
let relative_path = Path::new("packages/java/checkstyle.xml");
let changed = migrate_java_checkstyle_line_length(dir.path(), relative_path).expect("must not error");
assert!(
!changed,
"a LineLength max outside alef's two known historical defaults must never be touched"
);
let on_disk = std::fs::read_to_string(pkg_dir.join("checkstyle.xml")).expect("read file");
assert_eq!(
on_disk, customised,
"customised checkstyle.xml must survive byte-for-byte"
);
}
#[test]
fn should_not_touch_a_checkstyle_already_at_200() {
let dir = tempfile::tempdir().expect("tempdir");
let pkg_dir = dir.path().join("packages/java");
std::fs::create_dir_all(&pkg_dir).expect("create packages/java");
let current = checkstyle_with_line_length("<property name=\"max\" value=\"200\"/>");
std::fs::write(pkg_dir.join("checkstyle.xml"), ¤t).expect("write current checkstyle.xml");
let relative_path = Path::new("packages/java/checkstyle.xml");
let changed = migrate_java_checkstyle_line_length(dir.path(), relative_path).expect("must not error");
assert!(!changed);
}
#[test]
fn migrate_java_checkstyle_is_a_no_op_when_file_does_not_exist() {
let dir = tempfile::tempdir().expect("tempdir");
let relative_path = Path::new("packages/java/checkstyle.xml");
let changed = migrate_java_checkstyle_line_length(dir.path(), relative_path).expect("must not error");
assert!(!changed);
assert!(!dir.path().join(relative_path).exists());
}
}