#![cfg_attr(nightly, feature(proc_macro_span, proc_macro_diagnostic))]
use proc_macro;
use proc_macro::TokenStream;
use std::collections::HashSet;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::process::Command;
use quote::quote;
use error::handle_error;
use crate::error::CompileError;
use lockjaw_common::environment::{current_crate, current_package};
use lockjaw_common::manifest::LockjawPackage;
use lockjaw_common::manifest::{ComponentType, DepManifests, Manifest};
#[macro_use]
mod log;
mod component_visibles;
mod components;
mod entrypoints;
mod environment;
mod error;
mod graph;
mod injectables;
mod manifest;
mod modules;
mod nodes;
mod parsing;
mod qualifier;
mod type_data;
mod type_validator;
#[proc_macro_attribute]
pub fn injectable(attr: TokenStream, input: TokenStream) -> TokenStream {
handle_error(|| injectables::handle_injectable_attribute(attr.into(), input.into()))
}
#[proc_macro_attribute]
pub fn injectable_inject(_attr: TokenStream, _input: TokenStream) -> TokenStream {
doc_proc_macro("#[inject] should only annotate an item under a #[injectable] item. This attribute macro is for documentation purpose only and should not be called directly.")
}
#[proc_macro_attribute]
pub fn injectable_factory(_attr: TokenStream, _input: TokenStream) -> TokenStream {
doc_proc_macro("#[factory] should only annotate an item under a #[injectable] item. This attribute macro is for documentation purpose only and should not be called directly.")
}
#[proc_macro_attribute]
pub fn builder_modules(attr: TokenStream, input: TokenStream) -> TokenStream {
handle_error(|| components::handle_builder_modules_attribute(attr.into(), input.into()))
}
#[proc_macro_attribute]
pub fn component(attr: TokenStream, input: TokenStream) -> TokenStream {
handle_error(|| {
components::handle_component_attribute(attr.into(), input.into(), ComponentType::Component)
})
}
#[proc_macro_attribute]
pub fn subcomponent(attr: TokenStream, input: TokenStream) -> TokenStream {
handle_error(|| {
components::handle_component_attribute(
attr.into(),
input.into(),
ComponentType::Subcomponent,
)
})
}
#[proc_macro_attribute]
pub fn define_component(attr: TokenStream, input: TokenStream) -> TokenStream {
handle_error(|| {
components::handle_component_attribute(attr.into(), input.into(), ComponentType::Component)
})
}
#[proc_macro_attribute]
pub fn define_subcomponent(attr: TokenStream, input: TokenStream) -> TokenStream {
handle_error(|| {
components::handle_component_attribute(
attr.into(),
input.into(),
ComponentType::Subcomponent,
)
})
}
#[proc_macro_attribute]
pub fn component_qualified(_attr: TokenStream, _input: TokenStream) -> TokenStream {
doc_proc_macro("#[qualified] should only annotate an item under a #[component]/#[subcomponent]/#[define_component]/#[define_subcomponent] item. This attribute macro is for documentation purpose only and should not be called directly.")
}
#[proc_macro_attribute]
pub fn entry_point(attr: TokenStream, input: TokenStream) -> TokenStream {
handle_error(|| entrypoints::handle_entry_point_attribute(attr.into(), input.into()))
}
#[proc_macro_attribute]
pub fn module(attr: TokenStream, input: TokenStream) -> TokenStream {
handle_error(|| modules::handle_module_attribute(attr.into(), input.into()))
}
#[proc_macro_attribute]
pub fn module_provides(_attr: TokenStream, _input: TokenStream) -> TokenStream {
doc_proc_macro("#[provides] should only annotate an item under a #[module] item. This attribute macro is for documentation purpose only and should not be called directly.")
}
#[proc_macro_attribute]
pub fn module_binds(_attr: TokenStream, _input: TokenStream) -> TokenStream {
doc_proc_macro("#[binds] should only annotate an item under a #[module] item. This attribute macro is for documentation purpose only and should not be called directly.")
}
#[proc_macro_attribute]
pub fn module_binds_option_of(_attr: TokenStream, _input: TokenStream) -> TokenStream {
doc_proc_macro("#[binds_option_of] should only annotate an item under a #[module] item. This attribute macro is for documentation purpose only and should not be called directly.")
}
#[proc_macro_attribute]
pub fn module_multibinds(_attr: TokenStream, _input: TokenStream) -> TokenStream {
doc_proc_macro("#[multibinds] should only annotate an item under a #[module] item. This attribute macro is for documentation purpose only and should not be called directly.")
}
#[proc_macro_attribute]
pub fn module_into_vec(_attr: TokenStream, _input: TokenStream) -> TokenStream {
doc_proc_macro("#[into_vec] should only annotate an item under a #[module] item. This attribute macro is for documentation purpose only and should not be called directly.")
}
#[proc_macro_attribute]
pub fn module_elements_into_vec(_attr: TokenStream, _input: TokenStream) -> TokenStream {
doc_proc_macro("#[elements_into_vec] should only annotate an item under a #[module] item. This attribute macro is for documentation purpose only and should not be called directly.")
}
#[proc_macro_attribute]
pub fn module_into_map(_attr: TokenStream, _input: TokenStream) -> TokenStream {
doc_proc_macro("#[into_map] should only annotate an item under a #[module] item. This attribute macro is for documentation purpose only and should not be called directly.")
}
#[proc_macro_attribute]
pub fn module_qualified(_attr: TokenStream, _input: TokenStream) -> TokenStream {
doc_proc_macro("#[qualified] should only annotate an item under a #[module] item. This attribute macro is for documentation purpose only and should not be called directly.")
}
#[proc_macro_attribute]
pub fn qualifier(attr: TokenStream, input: TokenStream) -> TokenStream {
handle_error(|| qualifier::handle_qualifier_attribute(attr.into(), input.into()))
}
#[proc_macro_attribute]
pub fn component_visible(attr: TokenStream, input: TokenStream) -> TokenStream {
handle_error(|| {
component_visibles::handle_component_visible_attribute(attr.into(), input.into())
})
}
#[proc_macro]
pub fn epilogue(input: TokenStream) -> TokenStream {
let input2: proc_macro2::TokenStream = input.into();
let result = quote! {
#[cfg(not(any(test,doctest)))]
lockjaw::private_root_epilogue!(#input2);
#[cfg(any(test,doctest))]
lockjaw::private_test_epilogue!(#input2);
};
result.into()
}
#[derive(Default)]
struct EpilogueConfig {
for_test: bool,
debug_output: bool,
root: bool,
}
#[proc_macro]
pub fn private_root_epilogue(input: TokenStream) -> TokenStream {
handle_error(|| {
let mut config = EpilogueConfig {
..create_epilogue_config(input)
};
if current_package().eq("lockjaw") {
config.for_test = true;
config.root = true;
}
internal_epilogue(config)
})
}
#[proc_macro]
pub fn private_test_epilogue(input: TokenStream) -> TokenStream {
handle_error(|| {
let config = EpilogueConfig {
for_test: true,
root: true,
..create_epilogue_config(input)
};
internal_epilogue(config)
})
}
fn create_epilogue_config(input: TokenStream) -> EpilogueConfig {
let set: HashSet<String> = input.into_iter().map(|t| t.to_string()).collect();
EpilogueConfig {
debug_output: set.contains("debug_output"),
for_test: false,
root: std::env::var("CARGO_BIN_NAME").is_ok(),
..EpilogueConfig::default()
}
}
fn internal_epilogue(
config: EpilogueConfig,
) -> Result<proc_macro2::TokenStream, proc_macro2::TokenStream> {
let merged_manifest = merge_manifest(&config)?;
let expanded_visibilities = component_visibles::expand_visibilities(&merged_manifest)?;
let (components, initiazers, messages) =
components::generate_components(&merged_manifest, config.root)?;
let path_test;
if config.for_test {
path_test = quote! {}
} else {
path_test = quote! {
#[test]
fn epilogue_invoked_at_crate_root(){
let crate_name = module_path!().split("::").next().unwrap();
let mod_path = crate_name.to_owned();
assert_eq!(
module_path!(),
mod_path,
"lockjaw::epilogue!() called in a file other than lib.rs or main.rs"
);
}
};
}
let root_component_initializer = if config.root {
quote! {
#[doc(hidden)]
#[no_mangle]
#[allow(non_snake_case)]
pub(crate) fn lockjaw_init_root_components(){
#initiazers
}
}
} else {
quote! {}
};
let result = quote! {
#expanded_visibilities
#components
#path_test
#root_component_initializer
};
if config.debug_output {
let mut content = format!("/* manifest:\n{:#?}\n*/\n", merged_manifest);
for message in messages {
content.push_str(&format!("/*\n{}\n*/\n", message));
}
content.push_str(&result.to_string());
let path = format!(
"{}debug_{}.rs",
environment::lockjaw_output_dir()?,
current_crate()
);
log!(
"writing debug output to file:///{}",
path.replace("\\", "/")
);
std::fs::create_dir_all(Path::new(&environment::lockjaw_output_dir()?))
.expect("cannot create output dir");
std::fs::write(Path::new(&path), &content)
.expect(&format!("cannot write debug output to {}", path));
Command::new("rustfmt")
.arg(&path)
.output()
.map_compile_error("unable to format output")?;
Ok(quote! {
std::include!(#path);
})
} else {
Ok(result)
}
}
fn merge_manifest(config: &EpilogueConfig) -> Result<Manifest, proc_macro2::TokenStream> {
let mut result: Manifest = Manifest::new();
if let Ok(manifest) = std::env::var("LOCKJAW_TRYBUILD_PATH") {
let test_manifest = lockjaw_common::manifest_parser::parse_manifest(&LockjawPackage {
id: "".to_string(),
name: std::env::var("CARGO_PKG_NAME").unwrap().replace("-", "_"),
src_path: manifest,
direct_prod_crate_deps: vec![],
direct_test_crate_deps: vec![],
})
.test_manifest;
result.merge_from(&test_manifest);
return Ok(result);
}
if let Ok(manifest) = std::env::var("LOCKJAW_DEP_MANIFEST") {
let reader = BufReader::new(File::open(manifest).expect("cannot find manifest file"));
let dep_manifest: DepManifests =
serde_json::from_reader(reader).expect("cannot read manifest");
if config.for_test {
for dep in &dep_manifest.test_manifest {
result.merge_from(dep)
}
} else {
for dep in &dep_manifest.prod_manifest {
result.merge_from(dep)
}
}
if let Ok(bin_name) = std::env::var("CARGO_BIN_NAME") {
let root_manifest = dep_manifest
.root_manifests
.get(&bin_name)
.expect("CARGO_BIN_NAME not in manifest");
if config.for_test {
result.merge_from(&root_manifest.test_manifest);
} else {
result.merge_from(&root_manifest.prod_manifest);
}
} else {
if config.for_test {
let test_target = std::env::var("CARGO_CRATE_NAME").unwrap();
let test_manifest = &dep_manifest
.root_manifests
.get(&test_target)
.unwrap()
.test_manifest;
result.merge_from(&test_manifest);
} else {
result.merge_from(
&dep_manifest
.root_manifests
.get(&std::env::var("CARGO_CRATE_NAME").unwrap())
.expect("CARGO_CRATE_NAME not in manifest")
.prod_manifest,
)
}
}
} else {
return Err(
quote! { compile_error!("manifest missing, is the lockjaw::build_script called in build.rs?");},
);
}
Ok(result)
}
fn doc_proc_macro(message: &str) -> TokenStream {
(quote! { compile_error!(#message)}).into()
}