use crate::backend::Helpers;
use crate::compile::CompileError;
use crate::config::Config;
use crate::lexer::Lexer;
use std::collections::HashSet;
pub const MERGE_HELPER: &str = "__luaux_merge";
pub const READ_HELPER: &str = "__luaux_read";
const MERGE_HELPER_SOURCE: &str = "local function __luaux_merge(...): any local m, n = {}, 0 \
for i = 1, select(\"#\", ...) do local g = select(i, ...) if g ~= nil then for k, v in g do \
if type(k) == \"number\" then n += 1 m[n] = v else m[k] = v end end end end return m end";
const READ_HELPER_SOURCE: &str =
"local function __luaux_read(v) return if type(v) == \"function\" then (v :: () -> any)() else v end";
pub fn inject(
output: &str,
helpers: Helpers,
bound: &HashSet<String>,
config: &Config,
) -> Result<String, CompileError> {
if helpers.create {
let root = config
.create
.split(['.', ':'])
.next()
.unwrap_or(&config.create)
.trim();
if !bound.contains(root) {
return Err(CompileError {
message: format!("`{root}` is not in scope"),
offset: 0,
length: 0,
help: Some(format!(
"import it, or point [factory] create at something else \
(currently `{}`)",
config.create
)),
});
}
}
let mut statements = Vec::new();
if helpers.merge_props && !bound.contains(MERGE_HELPER) {
statements.push(MERGE_HELPER_SOURCE);
}
if helpers.read && !bound.contains(READ_HELPER) {
statements.push(READ_HELPER_SOURCE);
}
if statements.is_empty() {
return Ok(output.to_string());
}
let preamble = format!("{}; ", statements.join("; "));
Ok(match first_statement_offset(output) {
Some(offset) => format!("{}{preamble}{}", &output[..offset], &output[offset..]),
None => output.to_string(),
})
}
fn first_statement_offset(source: &str) -> Option<usize> {
let mut lexer = Lexer::new(source);
while let Some(token) = lexer.next_token() {
let token = token.ok()?;
if !token.is_trivia() {
return Some(token.start);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn bound(names: &[&str]) -> HashSet<String> {
names.iter().map(|name| name.to_string()).collect()
}
#[test]
fn the_merge_helper_has_a_usable_return_type() {
assert!(
MERGE_HELPER_SOURCE.contains("__luaux_merge(...): any"),
"{MERGE_HELPER_SOURCE}"
);
}
#[test]
fn the_read_helper_accepts_a_value_that_is_not_a_source() {
assert!(
READ_HELPER_SOURCE.contains("(v :: () -> any)()"),
"the cast is what detaches the call from the parameter's type: \
{READ_HELPER_SOURCE}"
);
assert!(
!READ_HELPER_SOURCE.contains("(v: any)"),
"an `any` parameter is optional, and costs the arity check: \
{READ_HELPER_SOURCE}"
);
}
#[test]
fn the_helpers_contain_no_newline() {
assert!(!MERGE_HELPER_SOURCE.contains('\n'), "{MERGE_HELPER_SOURCE}");
assert!(!READ_HELPER_SOURCE.contains('\n'), "{READ_HELPER_SOURCE}");
}
fn all() -> Helpers {
Helpers {
create: true,
read: true,
merge_props: true,
}
}
#[test]
fn inlines_helpers_with_no_config_and_no_dependency() {
let out = inject(
"local x = 1",
all(),
&bound(&["create"]),
&Config::default(),
)
.expect("inject");
assert!(out.contains("local function __luaux_merge"), "{out}");
assert!(out.contains("local function __luaux_read"), "{out}");
assert!(!out.contains("require"), "no dependency: {out}");
}
#[test]
fn inlines_only_what_is_used() {
let helpers = Helpers {
read: true,
..Default::default()
};
let out = inject("local x = 1", helpers, &bound(&[]), &Config::default()).expect("inject");
assert!(out.contains("__luaux_read"), "{out}");
assert!(!out.contains("__luaux_merge"), "{out}");
}
#[test]
fn respects_a_helper_the_author_already_defined() {
let out = inject(
"local x = 1",
all(),
&bound(&["create", MERGE_HELPER]),
&Config::default(),
)
.expect("inject");
assert!(!out.contains("local function __luaux_merge"), "{out}");
}
#[test]
fn requires_the_factory_to_be_in_scope() {
let helpers = Helpers {
create: true,
..Default::default()
};
let error = inject("local x = 1", helpers, &bound(&[]), &Config::default())
.expect_err("should fail");
assert!(
error.message.contains("`create` is not in scope"),
"{error:?}"
);
}
#[test]
fn checks_only_the_root_of_a_dotted_factory() {
let helpers = Helpers {
create: true,
..Default::default()
};
let config = Config::with_create("vide.create");
assert!(inject("local x = 1", helpers, &bound(&["vide"]), &config).is_ok());
let error =
inject("local x = 1", helpers, &bound(&["create"]), &config).expect_err("should fail");
assert!(
error.message.contains("`vide` is not in scope"),
"{error:?}"
);
}
#[test]
fn checks_the_object_a_method_factory_is_called_on() {
let helpers = Helpers {
create: true,
..Default::default()
};
let config = Config::with_create("scope:New");
assert!(inject("local x = 1", helpers, &bound(&["scope"]), &config).is_ok());
let error =
inject("local x = 1", helpers, &bound(&["New"]), &config).expect_err("should fail");
assert!(
error.message.contains("`scope` is not in scope"),
"{error:?}"
);
}
#[test]
fn injects_nothing_when_no_helper_is_used() {
let source = "local x = 1";
let out =
inject(source, Helpers::default(), &bound(&[]), &Config::default()).expect("inject");
assert_eq!(out, source);
}
#[test]
fn preserves_the_line_count() {
let source = "--!strict\nlocal x = 1\nreturn x";
let out = inject(source, all(), &bound(&["create"]), &Config::default()).expect("inject");
assert_eq!(out.lines().count(), source.lines().count(), "{out}");
}
#[test]
fn goes_after_leading_directives_and_comments() {
let source = "--!strict\n-- a note\n\nlocal x = 1";
let out = inject(source, all(), &bound(&["create"]), &Config::default()).expect("inject");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines[0], "--!strict");
assert_eq!(lines[1], "-- a note");
assert!(lines[3].ends_with("local x = 1"), "{out}");
}
#[test]
fn leaves_a_comment_only_file_alone() {
let source = "-- nothing here\n";
let out = inject(source, all(), &bound(&["create"]), &Config::default()).expect("inject");
assert_eq!(out, source);
}
}