1 // Copyright (c) Facebook, Inc. and its affiliates.
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};
32 file: RcOc<RelativePath>,
37 file: RcOc<RelativePath>,
38 start: Box<FilePosLarge>,
39 end: Box<FilePosLarge>,
42 file: RcOc<RelativePath>,
45 FromReason(Box<PosImpl>),
57 pub struct Pos(PosImpl);
59 pub type PosR<'a> = &'a Pos;
62 pub fn make_none() -> Self {
63 // TODO: shiqicao make NONE static, lazy_static doesn't allow Rc
65 file: RcOc::new(RelativePath::make(Prefix::Dummy, PathBuf::from(""))),
66 span: PosSpanTiny::make_dummy(),
70 pub fn is_none(&self) -> bool {
72 Pos(PosImpl::Tiny { file, span }) => span.is_dummy() && file.is_empty(),
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
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 });
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 });
96 start: Box::new(span.start),
97 end: Box::new(span.end),
101 fn to_raw_span(&self) -> PosSpanRaw {
103 PosImpl::Tiny { span, .. } => span.to_raw_span(),
104 PosImpl::Small { start, end, .. } => PosSpanRaw {
105 start: (*start).into(),
108 PosImpl::Large { start, end, .. } => PosSpanRaw {
112 PosImpl::FromReason(_p) => unimplemented!(),
116 pub fn filename(&self) -> &RelativePath {
117 self.filename_rc_ref()
120 fn filename_rc_ref(&self) -> &RcOc<RelativePath> {
122 PosImpl::Small { file, .. }
123 | PosImpl::Large { file, .. }
124 | PosImpl::Tiny { file, .. } => file,
125 PosImpl::FromReason(_p) => unimplemented!(),
129 fn into_filename(self) -> RcOc<RelativePath> {
131 PosImpl::Small { file, .. }
132 | PosImpl::Large { file, .. }
133 | PosImpl::Tiny { file, .. } => file,
134 PosImpl::FromReason(_p) => unimplemented!(),
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 {
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)
161 PosImpl::FromReason(_p) => unimplemented!(),
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!(),
174 (line_begin, line_end, start, end)
177 pub fn info_raw(&self) -> (usize, usize) {
178 (self.start_offset(), self.end_offset())
181 pub fn line(&self) -> usize {
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!(),
190 pub fn from_lnum_bol_offset(
191 file: RcOc<RelativePath>,
192 start: (usize, usize, usize),
193 end: (usize, usize, usize),
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 })
202 pub fn to_start_and_end_lnum_bol_offset(
204 ) -> ((usize, usize, usize), (usize, usize, usize)) {
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())
212 PosImpl::FromReason(_p) => unimplemented!(),
216 /// For single-line spans only.
217 pub fn from_line_cols_offset(
218 file: RcOc<RelativePath>,
223 let start = FilePosLarge::from_line_column_offset(line, cols.start, start_offset);
224 let end = FilePosLarge::from_line_column_offset(
227 start_offset + (cols.end - cols.start),
229 Self::from_raw_span(file, PosSpanRaw { start, end })
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 })
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()
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()
250 + &x2.end_offset().to_string())
252 Ok(Self::btw_nocheck(x1.clone(), x2.clone()))
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()
262 + &x2.filename().to_string());
265 let span1 = x1.to_raw_span();
266 let span2 = x2.to_raw_span();
268 let start = if span1.start.is_dummy() {
270 } else if span2.start.is_dummy() || span1.start.offset() < span2.start.offset() {
276 let end = if span1.end.is_dummy() {
278 } else if span2.end.is_dummy() || span1.end.offset() >= span2.end.offset() {
284 Ok(Self::from_raw_span(
285 RcOc::clone(x1.filename_rc_ref()),
286 PosSpanRaw { start, end },
290 pub fn last_char(&self) -> Cow<'_, Self> {
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 },
302 pub fn first_char_of_line(&self) -> Cow<'_, Self> {
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 },
314 pub fn end_offset(&self) -> usize {
316 PosImpl::Small { end, .. } => end.offset(),
317 PosImpl::Large { end, .. } => end.offset(),
318 PosImpl::Tiny { span, .. } => span.end_offset(),
319 PosImpl::FromReason(_p) => unimplemented!(),
323 pub fn start_offset(&self) -> usize {
325 PosImpl::Small { start, .. } => start.offset(),
326 PosImpl::Large { start, .. } => start.offset(),
327 PosImpl::Tiny { span, .. } => span.start_offset(),
328 PosImpl::FromReason(_p) => unimplemented!(),
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<'_>,
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)
347 write!(f, "({}:{}-{}:{})", start_line, start_col, end_line, end_col)
353 } => do_fmt(f, 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)
361 PosImpl::FromReason(_p) => unimplemented!(),
367 // Intended to match the implementation of `Pos.compare` in OCaml.
368 fn cmp(&self, other: &Pos) -> Ordering {
370 .cmp(other.filename())
371 .then(self.start_offset().cmp(&other.start_offset()))
372 .then(self.end_offset().cmp(&other.end_offset()))
376 impl PartialOrd for Pos {
377 fn partial_cmp(&self, other: &Pos) -> Option<Ordering> {
378 Some(self.cmp(other))
382 impl PartialEq for Pos {
383 fn eq(&self, other: &Self) -> bool {
384 self.cmp(other) == Ordering::Equal
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) {
397 impl EqModuloPos for Pos {
398 fn eq_modulo_pos(&self, _rhs: &Self) -> bool {
404 /// Returns a struct implementing Display which produces the same format as
405 /// `Pos.string` in OCaml.
406 pub fn string(&self) -> PosString<'_> {
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();
420 "File {:?}, line {}, characters {}-{}:",
421 self.0.filename().path(),
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) {}
435 pub type Map<T> = std::collections::BTreeMap<super::Pos, T>;
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))),
453 assert!(Pos::make_none().is_none());
455 !Pos::from_lnum_bol_offset(
456 RcOc::new(RelativePath::make(Prefix::Dummy, PathBuf::from("a"))),
463 !Pos::from_lnum_bol_offset(
464 RcOc::new(RelativePath::make(Prefix::Dummy, PathBuf::from(""))),
473 fn test_pos_string() {
475 Pos::make_none().string().to_string(),
476 r#"File "", line 0, characters 0-0:"#
478 let path = RcOc::new(RelativePath::make(Prefix::Dummy, PathBuf::from("a.php")));
480 Pos::from_lnum_bol_offset(path, (5, 100, 117), (5, 100, 142))
483 r#"File "a.php", line 5, characters 18-42:"#
488 fn test_pos_merge() {
489 let test = |name, (exp_start, exp_end), ((fst_start, fst_end), (snd_start, snd_end))| {
491 Ok(make_pos("a", exp_start, exp_end)),
493 &make_pos("a", fst_start, fst_end),
494 &make_pos("a", snd_start, snd_end)
500 // Run this again because we want to test that we get the same
501 // result regardless of order.
503 Ok(make_pos("a", exp_start, exp_end)),
505 &make_pos("a", snd_start, snd_end),
506 &make_pos("a", fst_start, fst_end),
515 ((0, 0, 0), (0, 0, 5)),
516 (((0, 0, 0), (0, 0, 2)), ((0, 0, 2), (0, 0, 5))),
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))),
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))),
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))),
538 Err("Position in separate files |a and |b".to_string()),
540 &make_pos("a", (0, 0, 0), (0, 0, 0)),
541 &make_pos("b", (0, 0, 0), (0, 0, 0))
543 "should reject merges with different filenames"