Skip to main content

meta_language/program_representation/
edit.rs

1use super::{
2    ProgramRange, ProgramRepresentation, ProgramRepresentationError, scope_by_id,
3    validate_identifier,
4};
5
6impl ProgramRepresentation {
7    /// Returns exact ranges of CST nodes with the requested grammar term.
8    #[must_use]
9    pub fn query_syntax(&self, term: &str) -> Vec<ProgramRange> {
10        self.source_mappings
11            .iter()
12            .filter(|mapping| mapping.term == term)
13            .map(|mapping| mapping.range)
14            .collect()
15    }
16
17    /// Replaces one exact source range and reparses the result.
18    pub fn replace(
19        &self,
20        range: ProgramRange,
21        replacement: &str,
22    ) -> Result<Self, ProgramRepresentationError> {
23        self.validate_range(range)?;
24        let mut edited = self.source.clone();
25        edited.replace_range(range.start..range.end, replacement);
26        self.reparse_edit(&edited)
27    }
28
29    /// Inserts target-language source at an exact byte boundary.
30    pub fn insert(
31        &self,
32        offset: usize,
33        inserted: &str,
34    ) -> Result<Self, ProgramRepresentationError> {
35        self.replace(ProgramRange::new(offset, offset), inserted)
36    }
37
38    /// Deletes one exact source range.
39    pub fn delete(&self, range: ProgramRange) -> Result<Self, ProgramRepresentationError> {
40        self.replace(range, "")
41    }
42
43    /// Clones one exact source range at an exact byte boundary.
44    pub fn clone_range(
45        &self,
46        range: ProgramRange,
47        destination: usize,
48    ) -> Result<Self, ProgramRepresentationError> {
49        self.validate_range(range)?;
50        self.insert(destination, &self.source[range.start..range.end])
51    }
52
53    /// Moves one exact range using byte offsets from the original source.
54    pub fn move_range(
55        &self,
56        range: ProgramRange,
57        destination: usize,
58    ) -> Result<Self, ProgramRepresentationError> {
59        self.validate_range(range)?;
60        self.validate_range(ProgramRange::new(destination, destination))?;
61        if destination > range.start && destination < range.end {
62            return Err(ProgramRepresentationError::DestinationInsideRange);
63        }
64        if destination == range.start || destination == range.end {
65            return Ok(self.clone());
66        }
67        let fragment = &self.source[range.start..range.end];
68        let mut without = self.source.clone();
69        without.replace_range(range.start..range.end, "");
70        let adjusted = if destination > range.end {
71            destination - (range.end - range.start)
72        } else {
73            destination
74        };
75        without.insert_str(adjusted, fragment);
76        self.reparse_edit(&without)
77    }
78
79    /// Renames exactly one resolved binding and reparses the edited source.
80    pub fn rename_binding(
81        &self,
82        binding_id: &str,
83        replacement: &str,
84    ) -> Result<Self, ProgramRepresentationError> {
85        let binding = self
86            .bindings
87            .iter()
88            .find(|binding| binding.id == binding_id)
89            .ok_or_else(|| ProgramRepresentationError::UnknownBinding(binding_id.to_string()))?;
90        validate_identifier(replacement, self.language)?;
91        if binding.name == replacement {
92            return Ok(self.clone());
93        }
94        let binding_scope = scope_by_id(&self.scopes, &binding.scope);
95        if let Some(conflict) = self.bindings.iter().find(|candidate| {
96            candidate.id != binding.id
97                && candidate.name == replacement
98                && (candidate.scope == binding.scope
99                    || (range_inside_scope(
100                        scope_by_id(&self.scopes, &candidate.scope).range,
101                        binding_scope.range,
102                    ) && binding.references.iter().any(|reference| {
103                        range_inside_scope(
104                            *reference,
105                            scope_by_id(&self.scopes, &candidate.scope).range,
106                        )
107                    }))
108                    || (range_inside_scope(
109                        binding_scope.range,
110                        scope_by_id(&self.scopes, &candidate.scope).range,
111                    ) && candidate
112                        .references
113                        .iter()
114                        .any(|reference| range_inside_scope(*reference, binding_scope.range))))
115        }) {
116            return Err(ProgramRepresentationError::CaptureConflict {
117                identifier: replacement.to_string(),
118                offset: conflict.declaration.start,
119            });
120        }
121        if let Some(reference) = self.unresolved_references.iter().find(|reference| {
122            reference.name == replacement
123                && reference.range.start >= binding_scope.range.start
124                && reference.range.end <= binding_scope.range.end
125        }) {
126            return Err(ProgramRepresentationError::CaptureConflict {
127                identifier: replacement.to_string(),
128                offset: reference.range.start,
129            });
130        }
131        let mut edits = std::iter::once(binding.declaration)
132            .chain(binding.references.iter().copied())
133            .map(|range| {
134                let shorthand = self.source_mappings.iter().any(|mapping| {
135                    mapping.range == range
136                        && shorthand_terms(self.language).contains(&mapping.term.as_str())
137                });
138                let text = if shorthand {
139                    format!("{}: {replacement}", binding.name)
140                } else {
141                    replacement.to_string()
142                };
143                (range, text)
144            })
145            .collect::<Vec<_>>();
146        edits.sort_by_key(|(range, _)| std::cmp::Reverse(range.start));
147        let mut edited = self.source.clone();
148        for (range, text) in &edits {
149            edited.replace_range(range.start..range.end, text);
150        }
151        let renamed = self.reparse_edit(&edited)?;
152        // Every other name must keep resolving exactly as before the rename.
153        if let Some(offset) =
154            first_resolution_change(self, &renamed, &binding.id, replacement, &edits)
155        {
156            return Err(ProgramRepresentationError::CaptureConflict {
157                identifier: replacement.to_string(),
158                offset,
159            });
160        }
161        Ok(renamed)
162    }
163
164    fn validate_range(&self, range: ProgramRange) -> Result<(), ProgramRepresentationError> {
165        if range.start > range.end
166            || range.end > self.source.len()
167            || !self.source.is_char_boundary(range.start)
168            || !self.source.is_char_boundary(range.end)
169        {
170            return Err(ProgramRepresentationError::InvalidRange {
171                start: range.start,
172                end: range.end,
173            });
174        }
175        Ok(())
176    }
177
178    fn reparse_edit(&self, source: &str) -> Result<Self, ProgramRepresentationError> {
179        let reparsed = Self::analyze(source, self.language, self.project.clone())?;
180        if !reparsed.network.verify_full_match(None).is_clean() {
181            return Err(ProgramRepresentationError::InvalidEdit);
182        }
183        Ok(reparsed)
184    }
185}
186
187fn shorthand_terms(language: &str) -> &'static [&'static str] {
188    match language {
189        "JavaScript" => &[
190            "shorthand_property_identifier",
191            "shorthand_property_identifier_pattern",
192        ],
193        "Rust" => &["shorthand_field_initializer"],
194        _ => &[],
195    }
196}
197
198#[derive(PartialEq, Eq)]
199enum Resolved<'a> {
200    Binding {
201        name: &'a str,
202        kind: &'a str,
203        declaration: ProgramRange,
204        references: Vec<ProgramRange>,
205    },
206    Unresolved {
207        name: &'a str,
208        range: ProgramRange,
209    },
210}
211
212impl Resolved<'_> {
213    const fn offset(&self) -> usize {
214        match self {
215            Self::Binding { declaration, .. } => declaration.start,
216            Self::Unresolved { range, .. } => range.start,
217        }
218    }
219}
220
221// Compares the resolution of the renamed program with the original one moved
222// through the rename edits; returns the first renamed-program offset whose
223// binding structure differs, or `None` when the rename preserved it.
224fn first_resolution_change(
225    original: &ProgramRepresentation,
226    renamed: &ProgramRepresentation,
227    binding_id: &str,
228    replacement: &str,
229    edits: &[(ProgramRange, String)],
230) -> Option<usize> {
231    let shift = |offset: usize| {
232        edits
233            .iter()
234            .filter(|(range, _)| range.end <= offset)
235            .fold(offset, |total, (range, text)| {
236                total + text.len() - (range.end - range.start)
237            })
238    };
239    let moved = |range: ProgramRange| {
240        let edit = edits.iter().find(|(candidate, _)| *candidate == range);
241        let from = shift(range.start) + edit.map_or(0, |(_, text)| text.len() - replacement.len());
242        let length = edit.map_or(range.end - range.start, |_| replacement.len());
243        ProgramRange::new(from, from + length)
244    };
245    let expected = original
246        .bindings
247        .iter()
248        .map(|candidate| Resolved::Binding {
249            name: if candidate.id == binding_id {
250                replacement
251            } else {
252                &candidate.name
253            },
254            kind: &candidate.kind,
255            declaration: moved(candidate.declaration),
256            references: candidate.references.iter().copied().map(moved).collect(),
257        })
258        .chain(
259            original
260                .unresolved_references
261                .iter()
262                .map(|reference| Resolved::Unresolved {
263                    name: &reference.name,
264                    range: moved(reference.range),
265                }),
266        )
267        .collect::<Vec<_>>();
268    let actual = renamed
269        .bindings
270        .iter()
271        .map(|candidate| Resolved::Binding {
272            name: &candidate.name,
273            kind: &candidate.kind,
274            declaration: candidate.declaration,
275            references: candidate.references.clone(),
276        })
277        .chain(
278            renamed
279                .unresolved_references
280                .iter()
281                .map(|reference| Resolved::Unresolved {
282                    name: &reference.name,
283                    range: reference.range,
284                }),
285        )
286        .collect::<Vec<_>>();
287    (0..expected.len().max(actual.len())).find_map(|index| {
288        let (left, right) = (expected.get(index), actual.get(index));
289        (left != right).then(|| right.or(left).map_or(0, Resolved::offset))
290    })
291}
292
293const fn range_inside_scope(range: ProgramRange, scope: ProgramRange) -> bool {
294    range.start >= scope.start && range.end <= scope.end
295}