Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions crates/workshop-rs/src/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,50 @@ impl SpanSlot for ActionProvenance {

/// The recorded span of one value node and its children, mirroring the
/// structure of the public [`Value`] tree.
/// Clear every recorded span in `provenance` that does not resolve inside
/// `document` — `SourceDocument::byte_range` already rejects spans whose file
/// or extent does not fit.
fn prune_spans_outside(provenance: &mut ProgramProvenance, document: &SourceDocument) {
fn prune_slot(slot: &mut Option<Span>, document: &SourceDocument) {
if let Some(span) = slot {
if document.byte_range(*span).is_none() {
*slot = None;
}
}
}
fn prune_value(record: &mut ValueProvenance, document: &SourceDocument) {
prune_slot(&mut record.span, document);
prune_slot(&mut record.identifier, document);
for child in &mut record.children {
prune_value(child, document);
}
}
for declaration in provenance
.global_variables
.iter_mut()
.chain(&mut provenance.player_variables)
.chain(&mut provenance.subroutines)
{
prune_slot(&mut declaration.span, document);
prune_slot(&mut declaration.name_span, document);
}
for rule in &mut provenance.rules {
prune_slot(&mut rule.span, document);
prune_slot(&mut rule.name, document);
prune_slot(&mut rule.event_name, document);
for condition in &mut rule.conditions {
prune_value(condition, document);
}
for action in &mut rule.actions {
prune_slot(&mut action.span, document);
prune_slot(&mut action.identifier, document);
for argument in &mut action.arguments {
prune_value(argument, document);
}
}
}
}

#[derive(Debug, Clone, Default)]
struct ValueProvenance {
span: Option<Span>,
Expand Down Expand Up @@ -235,11 +279,24 @@ impl Program {

/// Attach authored source text to a registered file. Returns `false` when
/// `file` is not a known file entry.
///
/// Attaching text audits provenance: a recorded span that does not
/// resolve inside the retained document is stale data — for a
/// provider-mapped program it can describe the pre-expansion token stream
/// or a coordinate space the map no longer owns — and is cleared so
/// consumers see no span rather than a wrong one, matching how
/// `record_identities` retires displaced records (wrightkit/wright#583).
pub fn set_file_source(&mut self, file: FileId, source: impl Into<String>) -> bool {
let Some(entry) = self.files.get_mut(file.index()) else {
return false;
};
entry.set_source(source);
if let (Some(provenance), Some(document)) = (
self.provenance.as_mut(),
self.files.get(file.index()).and_then(SourceFile::source),
) {
prune_spans_outside(provenance, document);
}
true
}

Expand Down
27 changes: 27 additions & 0 deletions crates/workshop-rs/tests/program_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,33 @@ fn applying_a_source_map_drops_stale_settings_spans() {
reparsed.validate().expect("mapped program validates");
}

#[test]
fn file_source_retention_prunes_spans_outside_the_text() {
// A provider map can record a span whose extent is not a position in the
// authored member — macro expansion attributes expanded text by shifting
// it onto the invocation site, so a recorded end column can run past the
// line (wrightkit/wright#583). Retaining the member's text clears such
// spans instead of letting them fail validation.
let workshop = "rule (\"tick\") {\n event { Ongoing - Each Player; All; All; }\n actions { Wait(1); }\n}\n";
let mut reparsed = parser::parse(workshop, &catalog(), &en()).expect("parses");
SourceMap::extract(&reparsed)
.apply(&mut reparsed)
.expect("map applies");
assert!(reparsed.rule_span(0).is_some());

// One line covers the `rule` header start but not the name span end.
assert!(reparsed.set_file_source(FileId::from_index(0), "rule (\"t"));
assert_eq!(reparsed.rule_span(0), None);
reparsed.validate().expect("mapped program validates");

// A span that fits the retained text survives.
let mut full = parser::parse(workshop, &catalog(), &en()).expect("parses");
SourceMap::extract(&full).apply(&mut full).expect("applies");
assert!(full.set_file_source(FileId::from_index(0), workshop));
assert!(full.rule_span(0).is_some());
full.validate().expect("in-bounds spans validate");
}

#[test]
fn values_and_conditions_are_composable() {
let value = Value::call(
Expand Down
Loading