Skip to content
Open
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repository = "https://github.com/wrightkit/wright"

[workspace.dependencies]
# Canonical Workshop core.
workshop-rs = "1.10.1"
workshop-rs = "1.12.0"
libc = "0.2"
serde = "1"
serde_json = "1"
Expand Down Expand Up @@ -55,3 +55,4 @@ debug = 1

[profile.test.package."*"]
debug = false

37 changes: 35 additions & 2 deletions crates/wright-analyzer/src/canonical/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,10 @@ fn duplicate_condition_findings(
/// read from the tick snapshot (random numbers, advancing clocks, sampled
/// server load) keep the branch reachable, so it stays.
///
/// Provenance alone cannot bound the branch: `Else If`, `Else`, and `End`
/// markers carry no action span, so the marker extents are derived
/// Branch extents come from marker provenance when the provider records it
/// (`Else If`/`Else` keywords and the `End` dedent position are authored
/// spans in the source language, #583) — [`marker_branch_range`]. Raw
/// Workshop markers carry no action span, so their extents are derived
/// textually against the retained source — see [`SourceScan`].
fn dead_branch_fix(
program: &Program,
Expand All @@ -406,6 +408,10 @@ fn dead_branch_fix(
.or_else(|| program.action_span(rule_id, action_id))?
.file;
let scan = SourceScan::new(program.source(file)?, file);
if let Some(range) = marker_branch_range(program, rule_id, rule, action_id, boundary, &scan) {
return dead_branch_span(&scan, range.start, range.end)
.map(|span| LintFix::RemoveDeadBranch { span });
}
let condition_end = expr_extent_end(
condition,
&mut |path| program.action_argument_value_span(rule_id, action_id, 0, path),
Expand Down Expand Up @@ -437,6 +443,33 @@ fn dead_branch_fix(
dead_branch_span(&scan, start, end).map(|span| LintFix::RemoveDeadBranch { span })
}

/// The dead branch's byte range taken from marker provenance: the provider's
/// recorded position for the dead `Else If` marker through the boundary
/// marker. Returns `None` unless the chain's `If` opener, the dead marker,
/// and the boundary marker all carry strictly increasing positions in the
/// scanned file — provenance where every marker inherits the same position
/// (or none) fails this check and the caller derives extents textually.
fn marker_branch_range(
program: &Program,
rule_id: RuleId,
rule: &Rule,
marker: usize,
boundary: usize,
scan: &SourceScan,
) -> Option<Range<usize>> {
let chain = chain_if_index(&rule.actions, marker)?;
let start = program.action_span(rule_id, marker)?;
let end = program.action_span(rule_id, boundary)?;
let head = program.action_span(rule_id, chain)?;
if start.file != scan.file || end.file != scan.file || head.file != scan.file {
return None;
}
let head = scan.byte(head.start)?;
let start = scan.byte(start.start)?;
let end = scan.byte(end.start)?;
(head < start && start < end).then_some(start..end)
}

/// The branch-removal span between a dead `Else If` marker and the next
/// chain marker, rounded to whole lines. `EditRange`s are half-open
/// line/column positions applied as a standard text splice, so whole-line
Expand Down
12 changes: 7 additions & 5 deletions crates/wright-driver/src/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -805,8 +805,10 @@ pub fn write_previews(
expected.insert(edit.source.as_str(), edit.source_identity.as_str());
}
for preview in previews {
let path = Path::new(&preview.source);
let bytes = std::fs::read(path).map_err(|error| {
// A provider-mapped edit names its source by `file://` URI (#583);
// `provider_uri_path` also passes plain path spellings through.
let path = PathBuf::from(crate::source_provider::provider_uri_path(&preview.source));
let bytes = std::fs::read(&path).map_err(|error| {
Diagnostic::error(
"input-io",
Stage::Discovery,
Expand All @@ -830,16 +832,16 @@ pub fn write_previews(
}
let mut written = Vec::new();
for preview in previews {
let path = Path::new(&preview.source);
let temporary = temporary_path(path);
let path = PathBuf::from(crate::source_provider::provider_uri_path(&preview.source));
let temporary = temporary_path(&path);
std::fs::write(&temporary, &preview.new_text).map_err(|error| {
Diagnostic::error(
"output-io",
Stage::Emission,
format!("cannot write '{}': {error}", temporary.display()),
)
})?;
std::fs::rename(&temporary, path).map_err(|error| {
std::fs::rename(&temporary, &path).map_err(|error| {
let _ = std::fs::remove_file(&temporary);
Diagnostic::error(
"output-io",
Expand Down
61 changes: 41 additions & 20 deletions crates/wright-driver/src/fix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
//! edits against the authored source, carrying the input identity, that
//! `validateEditTransaction` previews and `write_previews` applies. Fixes
//! exist only for `exact` findings whose correction is unambiguous
//! (`duplicate-condition`, `repeated-value`), and only for raw Workshop
//! input whose spans map to authored source.
//! (`duplicate-condition`, `repeated-value`), and only for input whose
//! spans map to retained authored source: raw Workshop parses, and
//! provider-mapped programs whose file table retains source text (#583).

use serde::Serialize;
use serde_json::{Value as JsonValue, json};
Expand Down Expand Up @@ -38,9 +39,9 @@ pub fn attach_fixes(
catalog: &Catalog,
locale: &Locale,
) {
// Fixes rewrite authored Workshop source only: a provider artifact's
// spans index generated text, not the caller's files.
if loaded.provenance != Provenance::Source {
// An unmapped provider artifact's spans index generated text, not the
// caller's files; a mapped artifact's spans address authored members.
if loaded.provenance == Provenance::Unmapped {
return;
}
for (finding, json) in findings.iter().zip(json.iter_mut()) {
Expand Down Expand Up @@ -111,20 +112,40 @@ fn materialize(
})
}

/// One plan span as a `SourceEdit` against the loaded input: the span must
/// map to the parsed text of file 0, the single authored source a raw
/// Workshop program has.
/// One plan span as a `SourceEdit` against the loaded input. For a raw
/// Workshop input the span must map to file 0's parsed text; for a
/// provider-mapped program the span's file indexes the source map's file
/// table — the edit names the member's `file://` URI and the identity of
/// the retained text, so `providerValidateEdits` and the write path
/// preconditions see the same bytes (#583).
fn edit(loaded: &Loaded, span: Span, new_text: String) -> Option<SourceEdit> {
(span.file.index() == 0).then(|| SourceEdit {
edit_kind: "fix".to_string(),
source: loaded.input.display.clone(),
source_identity: loaded.input.identity.clone(),
range: EditRange {
start_line: span.start.line,
start_col: span.start.col,
end_line: span.end.line,
end_col: span.end.col,
},
new_text,
})
let range = EditRange {
start_line: span.start.line,
start_col: span.start.col,
end_line: span.end.line,
end_col: span.end.col,
};
match loaded.provenance {
Provenance::Source => (span.file.index() == 0).then(|| SourceEdit {
edit_kind: "fix".to_string(),
source: loaded.input.display.clone(),
source_identity: loaded.input.identity.clone(),
range,
new_text,
}),
Provenance::Mapped => {
let member = loaded.source_files.get(span.file.index())?;
let (uri, _) =
crate::source_provider::provider_member(member, &loaded.input.cwd).ok()?;
let identity = crate::input_identity(loaded.program.source(span.file)?.text());
Some(SourceEdit {
edit_kind: "fix".to_string(),
source: uri,
source_identity: identity,
range,
new_text,
})
}
Provenance::Unmapped => None,
}
}
Loading
Loading