fix lint in oxidized
[hiphop-php.git] / hphp / hack / src / oxidized / manual / pos.rs
blob570034a3c4a42bf860dcf4a587f02f4091005c67
1 // Copyright (c) Facebook, Inc. and its affiliates.
2 //
3 // This source code is licensed under the MIT license found in the
4 // LICENSE file in the "hack" directory of this source tree.
6 use std::{borrow::Cow, cmp::Ordering, ops::Range, path::PathBuf};
8 use eq_modulo_pos::EqModuloPos;
9 use ocamlrep::rc::RcOc;
10 use ocamlrep_derive::{FromOcamlRep, FromOcamlRepIn, ToOcamlRep};
11 use serde::{Deserialize, Serialize};
13 use crate::file_pos::FilePos;
14 use crate::file_pos_large::FilePosLarge;
15 use crate::file_pos_small::FilePosSmall;
16 use crate::pos_span_raw::PosSpanRaw;
17 use crate::pos_span_tiny::PosSpanTiny;
18 use crate::relative_path::{Prefix, RelativePath};
20 #[derive(
21     Clone,
22     Debug,
23     Deserialize,
24     Hash,
25     FromOcamlRep,
26     FromOcamlRepIn,
27     ToOcamlRep,
28     Serialize
30 enum PosImpl {
31     Small {
32         file: RcOc<RelativePath>,
33         start: FilePosSmall,
34         end: FilePosSmall,
35     },
36     Large {
37         file: RcOc<RelativePath>,
38         start: Box<FilePosLarge>,
39         end: Box<FilePosLarge>,
40     },
41     Tiny {
42         file: RcOc<RelativePath>,
43         span: PosSpanTiny,
44     },
45     FromReason(Box<PosImpl>),
48 #[derive(
49     Clone,
50     Debug,
51     Deserialize,
52     FromOcamlRep,
53     FromOcamlRepIn,
54     ToOcamlRep,
55     Serialize
57 pub struct Pos(PosImpl);
59 pub type PosR<'a> = &'a Pos;
61 impl Pos {
62     pub fn make_none() -> Self {
63         // TODO: shiqicao make NONE static, lazy_static doesn't allow Rc
64         Pos(PosImpl::Tiny {
65             file: RcOc::new(RelativePath::make(Prefix::Dummy, PathBuf::from(""))),
66             span: PosSpanTiny::make_dummy(),
67         })
68     }
70     pub fn is_none(&self) -> bool {
71         match self {
72             Pos(PosImpl::Tiny { file, span }) => span.is_dummy() && file.is_empty(),
73             _ => false,
74         }
75     }
77     // Validness based on HHVM's definition
78     pub fn is_valid(&self) -> bool {
79         let (line0, line1, char0, char1) = self.info_pos_extended();
80         line0 != 1 || char0 != 1 || line1 != 1 || char1 != 1
81     }
83     fn from_raw_span(file: RcOc<RelativePath>, span: PosSpanRaw) -> Self {
84         if let Some(span) = PosSpanTiny::make(&span.start, &span.end) {
85             return Pos(PosImpl::Tiny { file, span });
86         }
87         let (lnum, bol, offset) = span.start.line_beg_offset();
88         if let Some(start) = FilePosSmall::from_lnum_bol_offset(lnum, bol, offset) {
89             let (lnum, bol, offset) = span.end.line_beg_offset();
90             if let Some(end) = FilePosSmall::from_lnum_bol_offset(lnum, bol, offset) {
91                 return Pos(PosImpl::Small { file, start, end });
92             }
93         }
94         Pos(PosImpl::Large {
95             file,
96             start: Box::new(span.start),
97             end: Box::new(span.end),
98         })
99     }
101     fn to_raw_span(&self) -> PosSpanRaw {
102         match &self.0 {
103             PosImpl::Tiny { span, .. } => span.to_raw_span(),
104             PosImpl::Small { start, end, .. } => PosSpanRaw {
105                 start: (*start).into(),
106                 end: (*end).into(),
107             },
108             PosImpl::Large { start, end, .. } => PosSpanRaw {
109                 start: **start,
110                 end: **end,
111             },
112             PosImpl::FromReason(_p) => unimplemented!(),
113         }
114     }
116     pub fn filename(&self) -> &RelativePath {
117         self.filename_rc_ref()
118     }
120     fn filename_rc_ref(&self) -> &RcOc<RelativePath> {
121         match &self.0 {
122             PosImpl::Small { file, .. }
123             | PosImpl::Large { file, .. }
124             | PosImpl::Tiny { file, .. } => file,
125             PosImpl::FromReason(_p) => unimplemented!(),
126         }
127     }
129     fn into_filename(self) -> RcOc<RelativePath> {
130         match self.0 {
131             PosImpl::Small { file, .. }
132             | PosImpl::Large { file, .. }
133             | PosImpl::Tiny { file, .. } => file,
134             PosImpl::FromReason(_p) => unimplemented!(),
135         }
136     }
138     /// Returns a closed interval that's incorrect for multi-line spans.
139     pub fn info_pos(&self) -> (usize, usize, usize) {
140         fn compute<P: FilePos>(pos_start: &P, pos_end: &P) -> (usize, usize, usize) {
141             let (line, start_minus1, bol) = pos_start.line_column_beg();
142             let start = start_minus1.wrapping_add(1);
143             let end_offset = pos_end.offset();
144             let mut end = end_offset - bol;
145             // To represent the empty interval, pos_start and pos_end are equal because
146             // end_offset is exclusive. Here, it's best for error messages to the user if
147             // we print characters N to N (highlighting a single character) rather than characters
148             // N to (N-1), which is very unintuitive.
149             if end == start_minus1 {
150                 end = start
151             }
152             (line, start, end)
153         }
154         match &self.0 {
155             PosImpl::Small { start, end, .. } => compute(start, end),
156             PosImpl::Large { start, end, .. } => compute(start.as_ref(), end.as_ref()),
157             PosImpl::Tiny { span, .. } => {
158                 let PosSpanRaw { start, end } = span.to_raw_span();
159                 compute(&start, &end)
160             }
161             PosImpl::FromReason(_p) => unimplemented!(),
162         }
163     }
165     pub fn info_pos_extended(&self) -> (usize, usize, usize, usize) {
166         let (line_begin, start, end) = self.info_pos();
167         let line_end = match &self.0 {
168             PosImpl::Small { end, .. } => end.line_column_beg(),
169             PosImpl::Large { end, .. } => (*end).line_column_beg(),
170             PosImpl::Tiny { span, .. } => span.to_raw_span().end.line_column_beg(),
171             PosImpl::FromReason(_p) => unimplemented!(),
172         }
173         .0;
174         (line_begin, line_end, start, end)
175     }
177     pub fn info_raw(&self) -> (usize, usize) {
178         (self.start_offset(), self.end_offset())
179     }
181     pub fn line(&self) -> usize {
182         match &self.0 {
183             PosImpl::Small { start, .. } => start.line(),
184             PosImpl::Large { start, .. } => start.line(),
185             PosImpl::Tiny { span, .. } => span.start_line_number(),
186             PosImpl::FromReason(_p) => unimplemented!(),
187         }
188     }
190     pub fn from_lnum_bol_offset(
191         file: RcOc<RelativePath>,
192         start: (usize, usize, usize),
193         end: (usize, usize, usize),
194     ) -> Self {
195         let (start_line, start_bol, start_offset) = start;
196         let (end_line, end_bol, end_offset) = end;
197         let start = FilePosLarge::from_lnum_bol_offset(start_line, start_bol, start_offset);
198         let end = FilePosLarge::from_lnum_bol_offset(end_line, end_bol, end_offset);
199         Self::from_raw_span(file, PosSpanRaw { start, end })
200     }
202     pub fn to_start_and_end_lnum_bol_offset(
203         &self,
204     ) -> ((usize, usize, usize), (usize, usize, usize)) {
205         match &self.0 {
206             PosImpl::Small { start, end, .. } => (start.line_beg_offset(), end.line_beg_offset()),
207             PosImpl::Large { start, end, .. } => (start.line_beg_offset(), end.line_beg_offset()),
208             PosImpl::Tiny { span, .. } => {
209                 let PosSpanRaw { start, end } = span.to_raw_span();
210                 (start.line_beg_offset(), end.line_beg_offset())
211             }
212             PosImpl::FromReason(_p) => unimplemented!(),
213         }
214     }
216     /// For single-line spans only.
217     pub fn from_line_cols_offset(
218         file: RcOc<RelativePath>,
219         line: usize,
220         cols: Range<usize>,
221         start_offset: usize,
222     ) -> Self {
223         let start = FilePosLarge::from_line_column_offset(line, cols.start, start_offset);
224         let end = FilePosLarge::from_line_column_offset(
225             line,
226             cols.end,
227             start_offset + (cols.end - cols.start),
228         );
229         Self::from_raw_span(file, PosSpanRaw { start, end })
230     }
232     pub fn btw_nocheck(x1: Self, x2: Self) -> Self {
233         let start = x1.to_raw_span().start;
234         let end = x2.to_raw_span().end;
235         Self::from_raw_span(x1.into_filename(), PosSpanRaw { start, end })
236     }
238     pub fn btw(x1: &Self, x2: &Self) -> Result<Self, String> {
239         if x1.filename() != x2.filename() {
240             // using string concatenation instead of format!,
241             // it is not stable see T52404885
242             Err(String::from("Position in separate files ")
243                 + &x1.filename().to_string()
244                 + " and "
245                 + &x2.filename().to_string())
246         } else if x1.end_offset() > x2.end_offset() {
247             Err(String::from("btw: invalid positions")
248                 + &x1.end_offset().to_string()
249                 + "and"
250                 + &x2.end_offset().to_string())
251         } else {
252             Ok(Self::btw_nocheck(x1.clone(), x2.clone()))
253         }
254     }
256     pub fn merge(x1: &Self, x2: &Self) -> Result<Self, String> {
257         if x1.filename() != x2.filename() {
258             // see comment above (T52404885)
259             return Err(String::from("Position in separate files ")
260                 + &x1.filename().to_string()
261                 + " and "
262                 + &x2.filename().to_string());
263         }
265         let span1 = x1.to_raw_span();
266         let span2 = x2.to_raw_span();
268         let start = if span1.start.is_dummy() {
269             span2.start
270         } else if span2.start.is_dummy() || span1.start.offset() < span2.start.offset() {
271             span1.start
272         } else {
273             span2.start
274         };
276         let end = if span1.end.is_dummy() {
277             span2.end
278         } else if span2.end.is_dummy() || span1.end.offset() >= span2.end.offset() {
279             span1.end
280         } else {
281             span2.end
282         };
284         Ok(Self::from_raw_span(
285             RcOc::clone(x1.filename_rc_ref()),
286             PosSpanRaw { start, end },
287         ))
288     }
290     pub fn last_char(&self) -> Cow<'_, Self> {
291         if self.is_none() {
292             Cow::Borrowed(self)
293         } else {
294             let end = self.to_raw_span().end;
295             Cow::Owned(Self::from_raw_span(
296                 RcOc::clone(self.filename_rc_ref()),
297                 PosSpanRaw { start: end, end },
298             ))
299         }
300     }
302     pub fn first_char_of_line(&self) -> Cow<'_, Self> {
303         if self.is_none() {
304             Cow::Borrowed(self)
305         } else {
306             let start = self.to_raw_span().start.with_column(0);
307             Cow::Owned(Self::from_raw_span(
308                 RcOc::clone(self.filename_rc_ref()),
309                 PosSpanRaw { start, end: start },
310             ))
311         }
312     }
314     pub fn end_offset(&self) -> usize {
315         match &self.0 {
316             PosImpl::Small { end, .. } => end.offset(),
317             PosImpl::Large { end, .. } => end.offset(),
318             PosImpl::Tiny { span, .. } => span.end_offset(),
319             PosImpl::FromReason(_p) => unimplemented!(),
320         }
321     }
323     pub fn start_offset(&self) -> usize {
324         match &self.0 {
325             PosImpl::Small { start, .. } => start.offset(),
326             PosImpl::Large { start, .. } => start.offset(),
327             PosImpl::Tiny { span, .. } => span.start_offset(),
328             PosImpl::FromReason(_p) => unimplemented!(),
329         }
330     }
333 impl std::fmt::Display for Pos {
334     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335         fn do_fmt<P: FilePos>(
336             f: &mut std::fmt::Formatter<'_>,
337             file: &RelativePath,
338             start: &P,
339             end: &P,
340         ) -> std::fmt::Result {
341             write!(f, "{}", file)?;
342             let (start_line, start_col, _) = start.line_column_beg();
343             let (end_line, end_col, _) = end.line_column_beg();
344             if start_line == end_line {
345                 write!(f, "({}:{}-{})", start_line, start_col, end_col)
346             } else {
347                 write!(f, "({}:{}-{}:{})", start_line, start_col, end_line, end_col)
348             }
349         }
350         match &self.0 {
351             PosImpl::Small {
352                 file, start, end, ..
353             } => do_fmt(f, file, start, end),
354             PosImpl::Large {
355                 file, start, end, ..
356             } => do_fmt(f, file, &**start, &**end),
357             PosImpl::Tiny { file, span } => {
358                 let PosSpanRaw { start, end } = span.to_raw_span();
359                 do_fmt(f, file, &start, &end)
360             }
361             PosImpl::FromReason(_p) => unimplemented!(),
362         }
363     }
366 impl Ord for Pos {
367     // Intended to match the implementation of `Pos.compare` in OCaml.
368     fn cmp(&self, other: &Pos) -> Ordering {
369         self.filename()
370             .cmp(other.filename())
371             .then(self.start_offset().cmp(&other.start_offset()))
372             .then(self.end_offset().cmp(&other.end_offset()))
373     }
376 impl PartialOrd for Pos {
377     fn partial_cmp(&self, other: &Pos) -> Option<Ordering> {
378         Some(self.cmp(other))
379     }
382 impl PartialEq for Pos {
383     fn eq(&self, other: &Self) -> bool {
384         self.cmp(other) == Ordering::Equal
385     }
388 impl Eq for Pos {}
390 // non-derived impl Hash because PartialEq and Eq are non-derived
391 impl std::hash::Hash for Pos {
392     fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
393         self.0.hash(hasher)
394     }
397 impl EqModuloPos for Pos {
398     fn eq_modulo_pos(&self, _rhs: &Self) -> bool {
399         true
400     }
403 impl Pos {
404     /// Returns a struct implementing Display which produces the same format as
405     /// `Pos.string` in OCaml.
406     pub fn string(&self) -> PosString<'_> {
407         PosString(self)
408     }
411 /// This struct has an impl of Display which produces the same format as
412 /// `Pos.string` in OCaml.
413 pub struct PosString<'a>(&'a Pos);
415 impl std::fmt::Display for PosString<'_> {
416     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417         let (line, start, end) = self.0.info_pos();
418         write!(
419             f,
420             "File {:?}, line {}, characters {}-{}:",
421             self.0.filename().path(),
422             line,
423             start,
424             end
425         )
426     }
429 // NoPosHash is meant to be position-insensitive, so don't do anything!
430 impl no_pos_hash::NoPosHash for Pos {
431     fn hash<H: std::hash::Hasher>(&self, _state: &mut H) {}
434 pub mod map {
435     pub type Map<T> = std::collections::BTreeMap<super::Pos, T>;
438 #[cfg(test)]
439 mod tests {
440     use super::*;
441     use pretty_assertions::assert_eq;
443     fn make_pos(name: &str, start: (usize, usize, usize), end: (usize, usize, usize)) -> Pos {
444         Pos::from_lnum_bol_offset(
445             RcOc::new(RelativePath::make(Prefix::Dummy, PathBuf::from(name))),
446             start,
447             end,
448         )
449     }
451     #[test]
452     fn test_pos() {
453         assert!(Pos::make_none().is_none());
454         assert!(
455             !Pos::from_lnum_bol_offset(
456                 RcOc::new(RelativePath::make(Prefix::Dummy, PathBuf::from("a"))),
457                 (0, 0, 0),
458                 (0, 0, 0)
459             )
460             .is_none(),
461         );
462         assert!(
463             !Pos::from_lnum_bol_offset(
464                 RcOc::new(RelativePath::make(Prefix::Dummy, PathBuf::from(""))),
465                 (1, 0, 0),
466                 (0, 0, 0)
467             )
468             .is_none(),
469         );
470     }
472     #[test]
473     fn test_pos_string() {
474         assert_eq!(
475             Pos::make_none().string().to_string(),
476             r#"File "", line 0, characters 0-0:"#
477         );
478         let path = RcOc::new(RelativePath::make(Prefix::Dummy, PathBuf::from("a.php")));
479         assert_eq!(
480             Pos::from_lnum_bol_offset(path, (5, 100, 117), (5, 100, 142))
481                 .string()
482                 .to_string(),
483             r#"File "a.php", line 5, characters 18-42:"#
484         );
485     }
487     #[test]
488     fn test_pos_merge() {
489         let test = |name, (exp_start, exp_end), ((fst_start, fst_end), (snd_start, snd_end))| {
490             assert_eq!(
491                 Ok(make_pos("a", exp_start, exp_end)),
492                 Pos::merge(
493                     &make_pos("a", fst_start, fst_end),
494                     &make_pos("a", snd_start, snd_end)
495                 ),
496                 "{}",
497                 name
498             );
500             // Run this again because we want to test that we get the same
501             // result regardless of order.
502             assert_eq!(
503                 Ok(make_pos("a", exp_start, exp_end)),
504                 Pos::merge(
505                     &make_pos("a", snd_start, snd_end),
506                     &make_pos("a", fst_start, fst_end),
507                 ),
508                 "{} (reversed)",
509                 name
510             );
511         };
513         test(
514             "basic test",
515             ((0, 0, 0), (0, 0, 5)),
516             (((0, 0, 0), (0, 0, 2)), ((0, 0, 2), (0, 0, 5))),
517         );
519         test(
520             "merge should work with gaps",
521             ((0, 0, 0), (0, 0, 15)),
522             (((0, 0, 0), (0, 0, 5)), ((0, 0, 10), (0, 0, 15))),
523         );
525         test(
526             "merge should work with overlaps",
527             ((0, 0, 0), (0, 0, 15)),
528             (((0, 0, 0), (0, 0, 12)), ((0, 0, 7), (0, 0, 15))),
529         );
531         test(
532             "merge should work between lines",
533             ((0, 0, 0), (2, 20, 25)),
534             (((0, 0, 0), (1, 10, 15)), ((1, 10, 20), (2, 20, 25))),
535         );
537         assert_eq!(
538             Err("Position in separate files |a and |b".to_string()),
539             Pos::merge(
540                 &make_pos("a", (0, 0, 0), (0, 0, 0)),
541                 &make_pos("b", (0, 0, 0), (0, 0, 0))
542             ),
543             "should reject merges with different filenames"
544         );
545     }