修复循环模块投影身份与内联体积门禁

按来源模块和原始根绑定传播投影声明身份
回流去重前把引用改接到既有 canonical 并拒绝未收敛固定点
将 inline module 纳入累计二 MiB JavaScript 预算
新增双端命名捕获循环与内联超限回归
同步技术方案与共享决策记录
This commit is contained in:
2026-08-04 13:00:31 +08:00
parent 865ff7e7d8
commit 5ea2dee663
4 changed files with 143 additions and 15 deletions
@@ -6423,26 +6423,74 @@ pub(in crate::agent) fn javascript_module_binding_projection(
projection
}
type JavascriptProjectionDeclarationIdentity = (String, String);
fn javascript_projection_declaration_identities(
module: &str,
analysis: &JavascriptModuleAnalysis,
) -> BTreeMap<String, JavascriptProjectionDeclarationIdentity> {
analysis
.root_bindings
.iter()
.chain(analysis.synthetic_declarations.keys())
.map(|binding| (binding.clone(), (module.to_string(), binding.clone())))
.collect()
}
fn javascript_remove_duplicate_projection_declarations(
projection: &mut String,
importer: &str,
projection_identities: &mut BTreeMap<String, JavascriptProjectionDeclarationIdentity>,
included_names: &BTreeMap<JavascriptProjectionDeclarationIdentity, String>,
) -> bool {
let projection_ranges = javascript_top_level_declaration_ranges(projection);
let importer_ranges = javascript_top_level_declaration_ranges(importer);
let mut duplicate_ranges = BTreeSet::new();
let mut declaration_bindings = BTreeMap::<(usize, usize), Vec<String>>::new();
for (binding, projection_range) in &projection_ranges {
let Some(importer_range) = importer_ranges.get(binding) else {
continue;
};
if projection.get(projection_range.clone()) == importer.get(importer_range.clone()) {
duplicate_ranges.insert((projection_range.start, projection_range.end));
}
declaration_bindings
.entry((projection_range.start, projection_range.end))
.or_default()
.push(binding.clone());
}
let duplicate_bindings = declaration_bindings
.values()
.filter(|bindings| {
bindings.iter().all(|binding| {
projection_identities
.get(binding)
.is_some_and(|identity| included_names.contains_key(identity))
})
})
.flatten()
.cloned()
.collect::<Vec<_>>();
let mut renamed_duplicates = BTreeSet::new();
for binding in &duplicate_bindings {
let Some(identity) = projection_identities.get(binding) else {
return false;
};
let Some(existing) = included_names.get(identity) else {
return false;
};
if binding != existing && !rename_javascript_root_binding(projection, binding, existing) {
return false;
}
renamed_duplicates.insert(existing.clone());
}
let duplicate_ranges = javascript_top_level_declaration_ranges(projection)
.into_iter()
.filter(|(binding, _)| renamed_duplicates.contains(binding))
.map(|(_, range)| (range.start, range.end))
.collect::<BTreeSet<_>>();
let mut replacements = duplicate_ranges
.into_iter()
.map(|(start, end)| (start..end, String::new()))
.collect::<Vec<_>>();
apply_javascript_span_replacements(projection, &mut replacements)
if !apply_javascript_span_replacements(projection, &mut replacements) {
return false;
}
for binding in duplicate_bindings {
projection_identities.remove(&binding);
}
true
}
pub(in crate::agent) fn apply_javascript_span_replacements(
@@ -6827,6 +6875,11 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
.enumerate()
{
let inline_id = format!("inline-module:{index}");
let inline_bytes = u64::try_from(inline_module.len()).unwrap_or(u64::MAX);
total_bytes = total_bytes
.checked_add(inline_bytes)
.filter(|bytes| *bytes <= MAX_EXTERNAL_GAMEPLAY_JAVASCRIPT_BYTES)
.ok_or_else(|| "自主构建外部脚本累计超过 2 MiB".to_string())?;
let analysis = javascript_module_analysis(&inline_module, true)
.ok_or_else(|| format!("自主构建内联模块不是有效 JavaScript:{inline_id}"))?;
let analysis = normalize_javascript_module_analysis_sources("game/index.html", analysis)?;
@@ -7156,10 +7209,24 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
.module_units
.extend(module_contents.values().cloned());
let mut projection_contents = module_contents.clone();
let original_projection_identities = module_contents
.iter()
.map(|(module, _)| {
let identities = module_analyses
.get(module)
.map(|analysis| javascript_projection_declaration_identities(module, analysis))
.unwrap_or_default();
(module.clone(), identities)
})
.collect::<BTreeMap<_, _>>();
let mut projection_identities = original_projection_identities.clone();
let mut projected_units = BTreeMap::<String, String>::new();
let mut projection_work_bytes = 0usize;
let mut projection_converged = false;
for _ in 0..module_bindings.len().max(1) {
let mut round_updates = BTreeMap::<String, String>::new();
let mut round_identity_updates =
BTreeMap::<String, BTreeMap<String, JavascriptProjectionDeclarationIdentity>>::new();
for (importer, dependencies) in &module_bindings {
let Some(importer_content) = module_contents.get(importer) else {
continue;
@@ -7284,10 +7351,23 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
}
let mut unit_root_bindings = importer_analysis.root_bindings.clone();
let mut unit_binding_names = importer_analysis.binding_names.clone();
let mut unit_projection_identities = original_projection_identities
.get(importer)
.cloned()
.unwrap_or_default();
let mut included_projection_names = unit_projection_identities
.iter()
.map(|(binding, identity)| (identity.clone(), binding.clone()))
.collect::<BTreeMap<_, _>>();
for (origin, bindings) in origins {
let Some(origin_content) = projection_contents.get(&origin) else {
continue;
};
let Some(origin_projection_identities) = projection_identities.get(&origin) else {
return Err(format!(
"自主构建模块投影缺少声明身份:{importer} <- {origin}"
));
};
let used_names = bindings
.iter()
.map(|(exported, _, _, _)| exported.clone())
@@ -7297,9 +7377,20 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
if projection.is_empty() {
continue;
}
let projection_ranges = javascript_top_level_declaration_ranges(&projection);
let mut selected_projection_identities = BTreeMap::new();
for binding in projection_ranges.keys() {
let Some(identity) = origin_projection_identities.get(binding) else {
return Err(format!(
"自主构建模块投影声明身份无法解析:{origin}::{binding}"
));
};
selected_projection_identities.insert(binding.clone(), identity.clone());
}
if !javascript_remove_duplicate_projection_declarations(
&mut projection,
importer_content,
&mut selected_projection_identities,
&included_projection_names,
) {
return Err(format!(
"自主构建循环模块投影声明范围无效:{importer} <- {origin}"
@@ -7381,6 +7472,15 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
));
}
}
for (original, canonical) in &projected_binding_names {
let Some(identity) = selected_projection_identities.get(original) else {
return Err(format!(
"自主构建模块投影重命名后缺少声明身份:{origin}::{original}"
));
};
included_projection_names.insert(identity.clone(), canonical.clone());
unit_projection_identities.insert(canonical.clone(), identity.clone());
}
if javascript_module_analysis(&projection, true).is_none() {
return Err(format!("自主构建模块投影重命名后语义无效:{origin}"));
}
@@ -7507,9 +7607,11 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
"自主构建模块投影累计处理超过 32 MiB,已拒绝继续展开".to_string()
})?;
round_updates.insert(importer.clone(), unit);
round_identity_updates.insert(importer.clone(), unit_projection_identities);
}
}
if round_updates.is_empty() {
projection_converged = true;
break;
}
let mut changed = false;
@@ -7520,10 +7622,15 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
}
projected_units.insert(importer, unit);
}
projection_identities.extend(round_identity_updates);
if !changed {
projection_converged = true;
break;
}
}
if !projection_converged {
return Err("自主构建模块投影未在有界轮次内收敛".to_string());
}
output.module_units.extend(projected_units.into_values());
Ok(output)
}
@@ -3772,12 +3772,12 @@ fn javascript_projection_preserves_nested_names_shorthand_keys_and_cycles() {
fs::write(
root.join("game/a.mjs"),
"import { b } from './b.mjs'; export function a() { return b(); }",
"import { b } from './b.mjs'; function decoyA() { const b = 0; return b; } export function a() { return b(); }",
)
.expect("write first cyclic module");
fs::write(
root.join("game/b.mjs"),
"import { a } from './a.mjs'; export function b() { return a(); }",
"import { a } from './a.mjs'; function decoyB() { const a = 0; return a; } export function b() { return a(); }",
)
.expect("write second cyclic module");
fs::write(
@@ -3791,7 +3791,9 @@ fn javascript_projection_preserves_nested_names_shorthand_keys_and_cycles() {
.module_units()
.iter()
.find(|unit| {
unit.contains("a();") && unit.contains("function a()") && unit.contains("function b()")
unit.contains("a();")
&& unit.contains("function a()")
&& unit.contains("function b__agc_import_1()")
})
.unwrap_or_else(|| {
panic!(
@@ -3800,10 +3802,26 @@ fn javascript_projection_preserves_nested_names_shorthand_keys_and_cycles() {
)
});
assert_eq!(projected.matches("function a()").count(), 1);
assert_eq!(projected.matches("function b()").count(), 1);
assert_eq!(projected.matches("function b__agc_import_1()").count(), 1);
assert!(!projected.contains("__agc_import_2"));
assert!(projected.contains("return a();"));
assert!(javascript_is_syntactically_valid(projected, true));
}
#[test]
fn javascript_inline_modules_share_the_external_script_byte_limit() {
let temporary = tempfile::tempdir().expect("create inline module limit project");
let oversized = " ".repeat(2 * 1024 * 1024 + 1);
let html = format!("<script type=\"module\">{oversized}</script>");
let error = read_external_gameplay_javascript_at(temporary.path(), &html)
.expect_err("inline modules must count toward the 2 MiB JavaScript limit");
assert!(
error.contains("累计超过 2 MiB"),
"unexpected error: {error}"
);
}
#[test]
fn javascript_alias_events_follow_function_invocation_time() {
let static_source = "import * as real from './real.mjs'; import * as decoy from './decoy.mjs'; let facade = real; function start() { return facade.run(); } start(); facade = decoy;";