Isolate Rust source facts
This commit is contained in:
@@ -12,17 +12,11 @@ Blocked by: none
|
|||||||
|
|
||||||
## HMX-M02 — Isolate Rust source fact extraction
|
## HMX-M02 — Isolate Rust source fact extraction
|
||||||
|
|
||||||
|
Outcome: Rust/syn context facts are privately owned without generated contract, ordering, fingerprint, or host-boundary drift.
|
||||||
Outcome: `hemx-build` Rust/syn fact collection has one private owner independent of template validation and emission.
|
|
||||||
Delta: architecture/002 assurance/001
|
Delta: architecture/002 assurance/001
|
||||||
Path: application Rust source -> syntax facts -> generated component/form/handler contract -> deterministic artifact.
|
Checks: focused build facts/contracts/fingerprint tests; Wasm graph; Redgate; diff check; fresh review.
|
||||||
Build: move the whole fact model, syn traversal, and related tests together; retain the existing host-only dependency boundary.
|
State: Done
|
||||||
Risk: reordered facts or changed path handling alters generated contracts or fingerprints.
|
Blocked by: none
|
||||||
Checks: `cargo test -p hemx-build`; generated-contract semantic and fingerprint fixtures; Wasm target tree gate; `redgate check`.
|
|
||||||
Non-goals: replacing syn, changing generated vocabulary, or introducing a generic analysis framework.
|
|
||||||
Residual risk: overlaps `hemx-build/src/lib.rs`; implement after HMX-M01 to avoid conflicting movement.
|
|
||||||
State: Draft
|
|
||||||
Blocked by: HMX-M01
|
|
||||||
|
|
||||||
## HMX-M03 — Isolate artifact emission and diagnostics
|
## HMX-M03 — Isolate artifact emission and diagnostics
|
||||||
|
|
||||||
|
|||||||
+9
-190
@@ -11,11 +11,20 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
mod authoring;
|
mod authoring;
|
||||||
|
mod rust_source;
|
||||||
|
|
||||||
use authoring::{
|
use authoring::{
|
||||||
reject_invalid_hemx_attr_placement, reject_invalid_hemx_attr_values,
|
reject_invalid_hemx_attr_placement, reject_invalid_hemx_attr_values,
|
||||||
reject_selector_target_attrs, reject_unkeyed_loop, reject_unknown_hemx_attrs,
|
reject_selector_target_attrs, reject_unkeyed_loop, reject_unknown_hemx_attrs,
|
||||||
};
|
};
|
||||||
|
#[cfg(test)]
|
||||||
|
use rust_source::{
|
||||||
|
collect_rust_struct_facts_from_file, compact_tokens, h_for_local_and_self_field,
|
||||||
|
vec_element_type,
|
||||||
|
};
|
||||||
|
use rust_source::{
|
||||||
|
context_type_for_heml_path, loop_locals_for_surface, nearest_dir_with, rust_struct_facts_in,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum DiagnosticSeverity {
|
pub enum DiagnosticSeverity {
|
||||||
@@ -92,12 +101,6 @@ pub struct TemplateLocalFact {
|
|||||||
pub fields: Vec<TemplateFieldFact>,
|
pub fields: Vec<TemplateFieldFact>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct RustStructFact {
|
|
||||||
fields: Vec<TemplateFieldFact>,
|
|
||||||
derives_hemplate: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn diagnostics_for_heml_file(path: impl AsRef<Path>) -> io::Result<Vec<Diagnostic>> {
|
pub fn diagnostics_for_heml_file(path: impl AsRef<Path>) -> io::Result<Vec<Diagnostic>> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let source = std::fs::read_to_string(path)?;
|
let source = std::fs::read_to_string(path)?;
|
||||||
@@ -1494,190 +1497,6 @@ fn is_inside_keyed_for(surface: &SurfaceDocument, mut scope: ScopeId) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn context_type_for_heml_path(path: &Path) -> Option<String> {
|
|
||||||
let stem = path.file_stem()?.to_str()?;
|
|
||||||
let mut out = String::new();
|
|
||||||
for word in stem.split(['_', '-']).filter(|word| !word.is_empty()) {
|
|
||||||
let mut chars = word.chars();
|
|
||||||
out.extend(chars.next()?.to_uppercase());
|
|
||||||
out.extend(chars);
|
|
||||||
}
|
|
||||||
(!out.is_empty()).then_some(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nearest_dir_with(path: &Path, file_name: &str) -> Option<PathBuf> {
|
|
||||||
for dir in path.ancestors().filter(|path| path.is_dir()) {
|
|
||||||
if dir.join(file_name).is_file() {
|
|
||||||
return Some(dir.to_path_buf());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rust_struct_facts_in(root: &Path) -> io::Result<HashMap<String, RustStructFact>> {
|
|
||||||
let mut facts = HashMap::new();
|
|
||||||
collect_rust_struct_facts(&root.join("src"), &mut facts)?;
|
|
||||||
Ok(facts)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_rust_struct_facts(
|
|
||||||
dir: &Path,
|
|
||||||
facts: &mut HashMap<String, RustStructFact>,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
for entry in entries {
|
|
||||||
let path = entry?.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
collect_rust_struct_facts(&path, facts)?;
|
|
||||||
} else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") {
|
|
||||||
collect_rust_struct_facts_from_file(&path, facts)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_rust_struct_facts_from_file(
|
|
||||||
path: &Path,
|
|
||||||
facts: &mut HashMap<String, RustStructFact>,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
let source = std::fs::read_to_string(path)?;
|
|
||||||
let file = match syn::parse_file(&source) {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(error) => return Err(io::Error::new(io::ErrorKind::InvalidData, error)),
|
|
||||||
};
|
|
||||||
collect_rust_struct_facts_from_items(&file.items, facts);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_rust_struct_facts_from_items(
|
|
||||||
items: &[syn::Item],
|
|
||||||
facts: &mut HashMap<String, RustStructFact>,
|
|
||||||
) {
|
|
||||||
for item in items {
|
|
||||||
match item {
|
|
||||||
syn::Item::Struct(item) => {
|
|
||||||
if let syn::Fields::Named(fields) = &item.fields {
|
|
||||||
facts.insert(
|
|
||||||
item.ident.to_string(),
|
|
||||||
RustStructFact {
|
|
||||||
fields: fields
|
|
||||||
.named
|
|
||||||
.iter()
|
|
||||||
.map(|field| TemplateFieldFact {
|
|
||||||
name: field.ident.as_ref().unwrap().to_string(),
|
|
||||||
type_name: compact_tokens(&field.ty),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
derives_hemplate: derives_hemplate(&item.attrs),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
syn::Item::Mod(item) => {
|
|
||||||
if let Some((_, items)) = &item.content {
|
|
||||||
collect_rust_struct_facts_from_items(items, facts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn derives_hemplate(attrs: &[syn::Attribute]) -> bool {
|
|
||||||
attrs
|
|
||||||
.iter()
|
|
||||||
.filter(|attr| attr.path().is_ident("derive"))
|
|
||||||
.filter_map(|attr| {
|
|
||||||
attr.parse_args_with(
|
|
||||||
syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
|
|
||||||
)
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
.any(|path| {
|
|
||||||
path.segments
|
|
||||||
.last()
|
|
||||||
.is_some_and(|segment| segment.ident == "Hemplate")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compact_tokens(tokens: &impl ToTokens) -> String {
|
|
||||||
tokens
|
|
||||||
.to_token_stream()
|
|
||||||
.to_string()
|
|
||||||
.replace(" :: ", "::")
|
|
||||||
.replace(" < ", "<")
|
|
||||||
.replace(" >", ">")
|
|
||||||
.replace(" ,", ",")
|
|
||||||
.replace(", ", ",")
|
|
||||||
.replace(" & ", "&")
|
|
||||||
.replace("& ", "&")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn loop_locals_for_surface(
|
|
||||||
surface: &SurfaceDocument,
|
|
||||||
self_fields: &[TemplateFieldFact],
|
|
||||||
structs: &HashMap<String, RustStructFact>,
|
|
||||||
) -> Vec<TemplateLocalFact> {
|
|
||||||
let mut locals = Vec::new();
|
|
||||||
for node in &surface.nodes {
|
|
||||||
let Some(value) = node
|
|
||||||
.attrs
|
|
||||||
.iter()
|
|
||||||
.find(|attr| attr.name == "h-for")
|
|
||||||
.and_then(|attr| attr.value.as_deref())
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Some((local, field)) = h_for_local_and_self_field(value) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Some(self_field) = self_fields.iter().find(|candidate| candidate.name == field) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Some(type_name) = vec_element_type(&self_field.type_name) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let fields = structs
|
|
||||||
.get(&type_name)
|
|
||||||
.map(|fact| fact.fields.clone())
|
|
||||||
.unwrap_or_default();
|
|
||||||
if !locals
|
|
||||||
.iter()
|
|
||||||
.any(|existing: &TemplateLocalFact| existing.name == local)
|
|
||||||
{
|
|
||||||
locals.push(TemplateLocalFact {
|
|
||||||
name: local,
|
|
||||||
type_name,
|
|
||||||
fields,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
locals
|
|
||||||
}
|
|
||||||
|
|
||||||
fn h_for_local_and_self_field(value: &str) -> Option<(String, String)> {
|
|
||||||
let (local, expr) = value.split_once(" in ")?;
|
|
||||||
let local = rust_ident(local.trim())?;
|
|
||||||
let expr = expr.trim().strip_prefix('&').unwrap_or(expr.trim()).trim();
|
|
||||||
let field = expr
|
|
||||||
.strip_prefix("self.")?
|
|
||||||
.split(['.', '(', '['])
|
|
||||||
.next()
|
|
||||||
.filter(|field| !field.is_empty())?;
|
|
||||||
Some((local, field.to_owned()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn vec_element_type(type_name: &str) -> Option<String> {
|
|
||||||
let inner = type_name
|
|
||||||
.strip_prefix("Vec<")
|
|
||||||
.or_else(|| type_name.strip_prefix("std::vec::Vec<"))?
|
|
||||||
.strip_suffix('>')?;
|
|
||||||
Some(inner.trim().to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unkeyed_generated_target_diagnostics(path: &Path, surface: &SurfaceDocument) -> Vec<Diagnostic> {
|
fn unkeyed_generated_target_diagnostics(path: &Path, surface: &SurfaceDocument) -> Vec<Diagnostic> {
|
||||||
let mut diagnostics = Vec::new();
|
let mut diagnostics = Vec::new();
|
||||||
for (node_scope, target) in generated_targets_with_scope(surface) {
|
for (node_scope, target) in generated_targets_with_scope(surface) {
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(super) struct RustStructFact {
|
||||||
|
pub(super) fields: Vec<TemplateFieldFact>,
|
||||||
|
pub(super) derives_hemplate: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn context_type_for_heml_path(path: &Path) -> Option<String> {
|
||||||
|
let stem = path.file_stem()?.to_str()?;
|
||||||
|
let mut out = String::new();
|
||||||
|
for word in stem.split(['_', '-']).filter(|word| !word.is_empty()) {
|
||||||
|
let mut chars = word.chars();
|
||||||
|
out.extend(chars.next()?.to_uppercase());
|
||||||
|
out.extend(chars);
|
||||||
|
}
|
||||||
|
(!out.is_empty()).then_some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn nearest_dir_with(path: &Path, file_name: &str) -> Option<PathBuf> {
|
||||||
|
for dir in path.ancestors().filter(|path| path.is_dir()) {
|
||||||
|
if dir.join(file_name).is_file() {
|
||||||
|
return Some(dir.to_path_buf());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn rust_struct_facts_in(root: &Path) -> io::Result<HashMap<String, RustStructFact>> {
|
||||||
|
let mut facts = HashMap::new();
|
||||||
|
collect_rust_struct_facts(&root.join("src"), &mut facts)?;
|
||||||
|
Ok(facts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_rust_struct_facts(
|
||||||
|
dir: &Path,
|
||||||
|
facts: &mut HashMap<String, RustStructFact>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
for entry in entries {
|
||||||
|
let path = entry?.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
collect_rust_struct_facts(&path, facts)?;
|
||||||
|
} else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") {
|
||||||
|
collect_rust_struct_facts_from_file(&path, facts)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn collect_rust_struct_facts_from_file(
|
||||||
|
path: &Path,
|
||||||
|
facts: &mut HashMap<String, RustStructFact>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let source = std::fs::read_to_string(path)?;
|
||||||
|
let file = match syn::parse_file(&source) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(error) => return Err(io::Error::new(io::ErrorKind::InvalidData, error)),
|
||||||
|
};
|
||||||
|
collect_rust_struct_facts_from_items(&file.items, facts);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_rust_struct_facts_from_items(
|
||||||
|
items: &[syn::Item],
|
||||||
|
facts: &mut HashMap<String, RustStructFact>,
|
||||||
|
) {
|
||||||
|
for item in items {
|
||||||
|
match item {
|
||||||
|
syn::Item::Struct(item) => {
|
||||||
|
if let syn::Fields::Named(fields) = &item.fields {
|
||||||
|
facts.insert(
|
||||||
|
item.ident.to_string(),
|
||||||
|
RustStructFact {
|
||||||
|
fields: fields
|
||||||
|
.named
|
||||||
|
.iter()
|
||||||
|
.map(|field| TemplateFieldFact {
|
||||||
|
name: field.ident.as_ref().unwrap().to_string(),
|
||||||
|
type_name: compact_tokens(&field.ty),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
derives_hemplate: derives_hemplate(&item.attrs),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syn::Item::Mod(item) => {
|
||||||
|
if let Some((_, items)) = &item.content {
|
||||||
|
collect_rust_struct_facts_from_items(items, facts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derives_hemplate(attrs: &[syn::Attribute]) -> bool {
|
||||||
|
attrs
|
||||||
|
.iter()
|
||||||
|
.filter(|attr| attr.path().is_ident("derive"))
|
||||||
|
.filter_map(|attr| {
|
||||||
|
attr.parse_args_with(
|
||||||
|
syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.any(|path| {
|
||||||
|
path.segments
|
||||||
|
.last()
|
||||||
|
.is_some_and(|segment| segment.ident == "Hemplate")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn compact_tokens(tokens: &impl ToTokens) -> String {
|
||||||
|
tokens
|
||||||
|
.to_token_stream()
|
||||||
|
.to_string()
|
||||||
|
.replace(" :: ", "::")
|
||||||
|
.replace(" < ", "<")
|
||||||
|
.replace(" >", ">")
|
||||||
|
.replace(" ,", ",")
|
||||||
|
.replace(", ", ",")
|
||||||
|
.replace(" & ", "&")
|
||||||
|
.replace("& ", "&")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn loop_locals_for_surface(
|
||||||
|
surface: &SurfaceDocument,
|
||||||
|
self_fields: &[TemplateFieldFact],
|
||||||
|
structs: &HashMap<String, RustStructFact>,
|
||||||
|
) -> Vec<TemplateLocalFact> {
|
||||||
|
let mut locals = Vec::new();
|
||||||
|
for node in &surface.nodes {
|
||||||
|
let Some(value) = node
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.find(|attr| attr.name == "h-for")
|
||||||
|
.and_then(|attr| attr.value.as_deref())
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some((local, field)) = h_for_local_and_self_field(value) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(self_field) = self_fields.iter().find(|candidate| candidate.name == field) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(type_name) = vec_element_type(&self_field.type_name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let fields = structs
|
||||||
|
.get(&type_name)
|
||||||
|
.map(|fact| fact.fields.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
if !locals
|
||||||
|
.iter()
|
||||||
|
.any(|existing: &TemplateLocalFact| existing.name == local)
|
||||||
|
{
|
||||||
|
locals.push(TemplateLocalFact {
|
||||||
|
name: local,
|
||||||
|
type_name,
|
||||||
|
fields,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
locals
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn h_for_local_and_self_field(value: &str) -> Option<(String, String)> {
|
||||||
|
let (local, expr) = value.split_once(" in ")?;
|
||||||
|
let local = rust_ident(local.trim())?;
|
||||||
|
let expr = expr.trim().strip_prefix('&').unwrap_or(expr.trim()).trim();
|
||||||
|
let field = expr
|
||||||
|
.strip_prefix("self.")?
|
||||||
|
.split(['.', '(', '['])
|
||||||
|
.next()
|
||||||
|
.filter(|field| !field.is_empty())?;
|
||||||
|
Some((local, field.to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn vec_element_type(type_name: &str) -> Option<String> {
|
||||||
|
let inner = type_name
|
||||||
|
.strip_prefix("Vec<")
|
||||||
|
.or_else(|| type_name.strip_prefix("std::vec::Vec<"))?
|
||||||
|
.strip_suffix('>')?;
|
||||||
|
Some(inner.trim().to_owned())
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ cargo test -p hemx-build generated_contract_fingerprint_is_deterministic --lib #
|
|||||||
cargo test -p hemx-build emits_generated_resources_from_heml --lib # spec: assurance/001 check
|
cargo test -p hemx-build emits_generated_resources_from_heml --lib # spec: assurance/001 check
|
||||||
cargo test -p hemx-build unkeyed_generated_target_diagnostic_is_structured --lib # spec: assurance/001 check
|
cargo test -p hemx-build unkeyed_generated_target_diagnostic_is_structured --lib # spec: assurance/001 check
|
||||||
test -f hemx-build/src/authoring.rs && grep -q '^mod authoring;$' hemx-build/src/lib.rs && ! grep -Eq '^fn (reject_selector_target_attrs|reject_unknown_hemx_attrs|known_hemx_attr|reject_invalid_hemx_attr_values|reject_invalid_hemx_attr_placement|reject_unkeyed_loop)\(' hemx-build/src/lib.rs # spec: architecture/001 check
|
test -f hemx-build/src/authoring.rs && grep -q '^mod authoring;$' hemx-build/src/lib.rs && ! grep -Eq '^fn (reject_selector_target_attrs|reject_unknown_hemx_attrs|known_hemx_attr|reject_invalid_hemx_attr_values|reject_invalid_hemx_attr_placement|reject_unkeyed_loop)\(' hemx-build/src/lib.rs # spec: architecture/001 check
|
||||||
|
test -f hemx-build/src/rust_source.rs && grep -q '^mod rust_source;$' hemx-build/src/lib.rs && ! grep -Eq '^fn (context_type_for_heml_path|nearest_dir_with|rust_struct_facts_in|collect_rust_struct_facts|compact_tokens|loop_locals_for_surface|h_for_local_and_self_field|vec_element_type)\(' hemx-build/src/lib.rs # spec: architecture/002 check
|
||||||
cargo test -p hemx-build no_op_build_preserves_generated_artifact_timestamps --lib # spec: build/003 check # spec: resource/005 check
|
cargo test -p hemx-build no_op_build_preserves_generated_artifact_timestamps --lib # spec: build/003 check # spec: resource/005 check
|
||||||
cargo test -p hemx-build changed_template_refreshes_generated_artifacts --lib # spec: resource/005 check
|
cargo test -p hemx-build changed_template_refreshes_generated_artifacts --lib # spec: resource/005 check
|
||||||
cargo test -p hemx-derive --test surface surface_macro_preserves_inline_module_with_generated_file -- --exact # spec: derive/001 check
|
cargo test -p hemx-derive --test surface surface_macro_preserves_inline_module_with_generated_file -- --exact # spec: derive/001 check
|
||||||
|
|||||||
Reference in New Issue
Block a user