use proc_macro2::{Delimiter, LineColumn, Spacing, Span, TokenStream, TokenTree};
use std::cell::RefCell;
use std::collections::HashMap;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use crate::diag::{Diagnostic, Diagnostics};
use crate::locate;
pub type Pos = u32;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct FileId(u32);
impl FileId {
pub fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct SourceRange {
pub start: Pos,
pub end: Pos,
}
impl SourceRange {
pub fn new(start: Pos, end: Pos) -> Self {
Self {
start,
end: if end < start { start } else { end },
}
}
pub fn at(pos: Pos) -> Self {
Self {
start: pos,
end: pos,
}
}
pub fn join(self, other: Self) -> Self {
Self {
start: self.start.min(other.start),
end: self.end.max(other.end),
}
}
pub fn len(self) -> u32 {
self.end - self.start
}
pub fn is_empty(self) -> bool {
self.start == self.end
}
}
pub(crate) type AnchorList = Vec<(u32, u32, Span)>;
#[derive(Clone)]
pub struct Subspan(Rc<SubspanInner>);
struct SubspanInner {
resolve: Box<dyn Fn(Range<usize>) -> Option<Span>>,
cache: RefCell<HashMap<(u32, u32), Option<Span>>>,
}
impl Subspan {
pub fn new(resolve: impl Fn(Range<usize>) -> Option<Span> + 'static) -> Self {
Self(Rc::new(SubspanInner {
resolve: Box::new(resolve),
cache: RefCell::new(HashMap::new()),
}))
}
fn get(&self, start: u32, end: u32) -> Option<Span> {
if let Some(hit) = self.0.cache.borrow().get(&(start, end)) {
return *hit;
}
let span = (self.0.resolve)(start as usize..end as usize);
self.0.cache.borrow_mut().insert((start, end), span);
span
}
}
impl std::fmt::Debug for Subspan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Subspan")
}
}
#[derive(Clone, Default, Debug)]
pub struct Origin {
entry_names: &'static [&'static str],
crate_dir: Option<PathBuf>,
subspan: Option<Subspan>,
}
impl Origin {
pub fn unknown() -> Self {
Self::default()
}
pub fn new(entry_names: &'static [&'static str]) -> Self {
Self {
entry_names,
crate_dir: std::env::var_os(crate::include::MANIFEST_DIR_VAR).map(PathBuf::from),
subspan: None,
}
}
pub fn with_subspan(mut self, subspan: Option<Subspan>) -> Self {
self.subspan = subspan;
self
}
pub fn in_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.crate_dir = Some(dir.into());
self
}
pub fn entry_names(&self) -> &'static [&'static str] {
self.entry_names
}
pub fn crate_dir(&self) -> Option<&Path> {
self.crate_dir.as_deref()
}
fn entry_name(&self) -> &str {
self.entry_names.first().copied().unwrap_or("c99")
}
fn search(&self, toks: &[FlatTok], accept: impl FnMut(&Path) -> bool) -> Option<locate::Slice> {
let dir = self.crate_dir.as_deref()?;
locate::Search {
dir,
entry_names: self.entry_names,
caps: locate::Caps::default(),
}
.find(toks, accept)
}
}
enum Spelling {
Shift(u32),
Table(Vec<u32>),
}
impl Spelling {
fn range(&self, start: u32, end: u32) -> Option<(u32, u32)> {
match self {
Spelling::Shift(shift) => Some((shift + start, shift + end)),
Spelling::Table(table) => {
Some((*table.get(start as usize)?, *table.get(end as usize)?))
}
}
}
}
struct PreciseSpans {
hook: Subspan,
spelling: Spelling,
}
impl PreciseSpans {
fn span(&self, start: u32, end: u32, len: u32) -> Option<Span> {
let mut start = start.min(len);
let mut end = end.clamp(start, len);
if start == end {
if end < len {
end += 1;
} else if start > 0 {
start -= 1;
} else {
return None;
}
}
let (start, end) = self.spelling.range(start, end)?;
if end <= start {
return None;
}
self.hook.get(start, end)
}
}
#[derive(Clone, Copy)]
struct Anchor {
start: Pos,
end: Pos,
span: Span,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum InputMode {
StringLiteral,
FileSlice,
Reconstructed,
Included,
CFile,
}
struct FileSpec {
name: String,
text: String,
anchors: AnchorList,
fallback_span: Span,
precise: bool,
mode: InputMode,
rust_path: Option<String>,
first_line: usize,
precise_spans: Option<PreciseSpans>,
}
pub struct SourceFile {
id: FileId,
name: String,
base: Pos,
text: String,
line_starts: Vec<u32>,
anchors: Vec<Anchor>,
fallback_span: Span,
precise: bool,
precise_spans: Option<PreciseSpans>,
mode: InputMode,
rust_path: Option<String>,
first_line: usize,
}
impl SourceFile {
pub fn id(&self) -> FileId {
self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn base(&self) -> Pos {
self.base
}
pub fn text(&self) -> &str {
&self.text
}
pub fn range(&self) -> SourceRange {
SourceRange::new(self.base, self.base + self.text.len() as Pos)
}
pub fn precise(&self) -> bool {
self.precise
}
pub fn mode(&self) -> InputMode {
self.mode
}
pub fn line_count(&self) -> usize {
self.line_starts.len()
}
pub fn rust_path(&self) -> Option<&str> {
self.rust_path.as_deref()
}
pub fn first_line(&self) -> usize {
self.first_line
}
pub fn is_included(&self) -> bool {
matches!(self.mode, InputMode::Included | InputMode::CFile)
}
fn local_line_col(&self, local: u32) -> (usize, usize) {
let local = local.min(self.text.len() as u32);
let line = match self.line_starts.binary_search(&local) {
Ok(i) => i,
Err(i) => i.saturating_sub(1),
};
let line_start = self.line_starts[line] as usize;
let col = self
.text
.get(line_start..local as usize)
.map_or(0, |s| s.chars().count())
+ 1;
(line + 1, col)
}
}
pub struct SourceMap {
files: Vec<SourceFile>,
next_base: Pos,
}
impl Default for SourceMap {
fn default() -> Self {
Self::new()
}
}
impl SourceMap {
pub fn new() -> Self {
Self {
files: Vec::new(),
next_base: 0,
}
}
fn add_file(&mut self, spec: FileSpec) -> FileId {
let id = FileId(self.files.len() as u32);
let base = self.next_base;
let text = spec.text;
let mut line_starts = vec![0u32];
for (i, b) in text.bytes().enumerate() {
if b == b'\n' {
line_starts.push(i as u32 + 1);
}
}
let anchors = spec
.anchors
.into_iter()
.map(|(s, e, span)| Anchor {
start: base + s,
end: base + e,
span,
})
.collect();
self.next_base = base.saturating_add(text.len() as Pos).saturating_add(1);
self.files.push(SourceFile {
id,
name: spec.name,
base,
text,
line_starts,
anchors,
fallback_span: spec.fallback_span,
precise: spec.precise,
precise_spans: spec.precise_spans,
mode: spec.mode,
rust_path: spec.rust_path,
first_line: spec.first_line.max(1),
});
id
}
pub fn next_base(&self) -> Pos {
self.next_base
}
pub fn add_included_file(
&mut self,
name: impl Into<String>,
text: String,
directive_span: Span,
) -> FileId {
self.add_file(FileSpec {
name: name.into(),
text,
anchors: Vec::new(),
fallback_span: directive_span,
precise: false,
mode: InputMode::Included,
rust_path: None,
first_line: 1,
precise_spans: None,
})
}
pub fn files(&self) -> &[SourceFile] {
&self.files
}
pub fn file(&self, id: FileId) -> &SourceFile {
&self.files[id.index()]
}
pub fn file_of(&self, pos: Pos) -> FileId {
debug_assert!(!self.files.is_empty(), "source map has no files");
let idx = self
.files
.partition_point(|f| f.base <= pos)
.saturating_sub(1);
FileId(idx as u32)
}
pub fn is_precise(&self, pos: Pos) -> bool {
self.file(self.file_of(pos)).precise
}
pub fn is_precise_at(&self, range: SourceRange) -> bool {
self.resolve(range).1
}
pub fn line_col(&self, pos: Pos) -> (usize, usize) {
let file = self.file(self.file_of(pos));
file.local_line_col(pos.saturating_sub(file.base))
}
pub fn header_position(&self, pos: Pos) -> Option<(&str, usize, usize)> {
let file = self.file(self.file_of(pos));
if !file.is_included() {
return None;
}
let (line, column) = file.local_line_col(pos.saturating_sub(file.base));
Some((&file.name, line, column))
}
pub fn source_line(&self, pos: Pos) -> usize {
let file = self.file(self.file_of(pos));
let (line, _) = file.local_line_col(pos.saturating_sub(file.base));
file.first_line + line - 1
}
pub fn span(&self, range: SourceRange) -> Span {
self.resolve(range).0
}
pub fn resolve(&self, range: SourceRange) -> (Span, bool) {
if self.files.is_empty() {
return (Span::call_site(), false);
}
let file = self.file(self.file_of(range.start));
if let Some(precise) = &file.precise_spans
&& let Some(span) = precise.span(
range.start.saturating_sub(file.base),
range.end.saturating_sub(file.base),
file.text.len() as Pos,
)
{
return (span, true);
}
(self.anchored_span(file, range), file.precise)
}
fn anchored_span(&self, file: &SourceFile, range: SourceRange) -> Span {
if file.anchors.is_empty() {
return file.fallback_span;
}
let idx = file.anchors.partition_point(|a| a.start <= range.start);
if idx > 0 {
let a = &file.anchors[idx - 1];
if a.end > range.start {
return a.span;
}
}
let before = idx.checked_sub(1).map(|i| &file.anchors[i]);
let after = file.anchors.get(idx);
if let Some(a) = after
&& a.start < range.end
{
return a.span;
}
match (before, after) {
(Some(b), Some(a)) => {
if range.start - b.end <= a.start - range.start {
b.span
} else {
a.span
}
}
(Some(b), None) => b.span,
(None, Some(a)) => a.span,
(None, None) => file.fallback_span,
}
}
}
pub struct Source {
pub map: SourceMap,
pub root: FileId,
pub mode: InputMode,
unit_id: u64,
}
impl Source {
pub fn unit_id(&self) -> u64 {
self.unit_id
}
}
impl Source {
pub fn text(&self) -> &str {
self.map.file(self.root).text()
}
pub fn base(&self) -> Pos {
self.map.file(self.root).base()
}
pub fn root_range(&self) -> SourceRange {
self.map.file(self.root).range()
}
}
pub fn capture(input: TokenStream, diags: &mut Diagnostics) -> Source {
capture_with(input, diags, &Origin::unknown())
}
pub fn capture_with(input: TokenStream, diags: &mut Diagnostics, origin: &Origin) -> Source {
let trees: Vec<TokenTree> = input.into_iter().collect();
if trees.len() == 1
&& let TokenTree::Literal(lit) = &trees[0]
{
let repr = lit.to_string();
if is_string_literal(&repr) {
let span = lit.span();
let hook = origin
.subspan
.clone()
.filter(|_| span.source_text().is_none_or(|text| text == repr));
let want_spelling = hook.is_some();
let (text, spelling, error) = match decode_string_literal(&repr, want_spelling) {
Ok((text, spelling)) => (text, spelling, None),
Err(msg) => (String::new(), None, Some(msg)),
};
let precise_spans = match (hook, spelling) {
(Some(hook), Some(spelling)) => Some(PreciseSpans { hook, spelling }),
_ => None,
};
let mut written_in = rust_path_of(span).map(|path| (path, span.start()));
if written_in.is_none() {
let mut toks = Vec::new();
flatten(vec![trees[0].clone()], &mut toks);
written_in = origin.search(&toks, |_| true).map(|slice| {
(
slice.path.display().to_string(),
LineColumn {
line: slice.line,
column: slice.column,
},
)
});
}
let (rust_path, at) = match written_in {
Some((path, at)) => (Some(path), at),
None => (None, span.start()),
};
let mut map = SourceMap::new();
let root = map.add_file(FileSpec {
name: "<c99! string literal>".to_owned(),
text,
anchors: Vec::new(),
fallback_span: span,
precise: false,
precise_spans,
mode: InputMode::StringLiteral,
rust_path: rust_path.clone(),
first_line: at.line,
});
let unit_id = unit_id_of(rust_path.as_deref(), at, map.file(root).text());
let source = Source {
map,
root,
mode: InputMode::StringLiteral,
unit_id,
};
if let Some(msg) = error {
diags.error(SourceRange::at(source.base()), msg);
}
return source;
}
}
let mut toks = Vec::new();
flatten(trees, &mut toks);
let fallback_span = toks.first().map_or_else(Span::call_site, |t| t.span);
let positioned = positions_are_usable(&toks);
let located = capture_file_slice(&toks).or_else(|| {
if positioned {
return None;
}
origin.search(&toks, |_| true).map(Located::from)
});
if let Some(located) = located {
let name = located.path.display().to_string();
let at = LineColumn {
line: located.line,
column: located.column,
};
let mut map = SourceMap::new();
let root = map.add_file(FileSpec {
rust_path: Some(name.clone()),
name,
text: located.text,
anchors: located.anchors,
fallback_span,
precise: true,
precise_spans: None,
mode: InputMode::FileSlice,
first_line: located.line,
});
let unit_id = unit_id_of(
Some(map.file(root).rust_path().expect("just set")),
at,
map.file(root).text(),
);
return Source {
map,
root,
mode: InputMode::FileSlice,
unit_id,
};
}
let rebuilt = if positioned {
reconstruct(&toks)
} else {
reconstruct_from_tokens(&toks, origin.entry_name())
};
let fallback_span = rebuilt
.blocked
.as_ref()
.map_or(fallback_span, |blocked| blocked.span);
let first_line = toks.first().map_or(1, |t| t.span.start().line);
let mut map = SourceMap::new();
let root = map.add_file(FileSpec {
name: "<c99! macro input>".to_owned(),
text: rebuilt.text,
anchors: rebuilt.anchors,
fallback_span,
precise: true,
precise_spans: None,
mode: InputMode::Reconstructed,
rust_path: rust_path_of(fallback_span),
first_line,
});
let unit_id = unit_id_of(
map.file(root).rust_path(),
fallback_span.start(),
map.file(root).text(),
);
let source = Source {
map,
root,
mode: InputMode::Reconstructed,
unit_id,
};
if let Some(blocked) = rebuilt.blocked {
let mut diag = Diagnostic::error(SourceRange::at(source.base()), blocked.message);
for note in blocked.notes {
diag = diag.with_note(note);
}
diags.push(diag);
}
source
}
pub fn invocation_directory(
input: &TokenTree,
origin: &Origin,
mut accept: impl FnMut(&Path) -> bool,
) -> Option<PathBuf> {
let mut toks = Vec::new();
flatten(vec![input.clone()], &mut toks);
let slice = origin.search(&toks, |path| path.parent().is_some_and(&mut accept))?;
slice.path.parent().map(Path::to_path_buf)
}
pub fn capture_c_file(name: String, text: String, span: Span) -> Source {
let mut map = SourceMap::new();
let unit_id = unit_id_of(rust_path_of(span).as_deref(), span.start(), &text);
let root = map.add_file(FileSpec {
rust_path: Some(name.clone()),
name,
text,
anchors: Vec::new(),
fallback_span: span,
precise: false,
precise_spans: None,
mode: InputMode::CFile,
first_line: 1,
});
Source {
map,
root,
mode: InputMode::CFile,
unit_id,
}
}
fn rust_path_of(span: Span) -> Option<String> {
span.local_file().map(|p| p.display().to_string())
}
fn unit_id_of(path: Option<&str>, at: LineColumn, text: &str) -> u64 {
let mut hash = FNV_OFFSET;
if let Some(path) = path {
hash = fnv(hash, path.as_bytes());
}
hash = fnv(hash, &(at.line as u64).to_le_bytes());
hash = fnv(hash, &(at.column as u64).to_le_bytes());
fnv(hash, text.as_bytes())
}
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
fn fnv(mut hash: u64, bytes: &[u8]) -> u64 {
for b in bytes {
hash ^= u64::from(*b);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
fn is_string_literal(repr: &str) -> bool {
repr.starts_with('"') || repr.starts_with("r\"") || repr.starts_with("r#")
}
pub fn string_literal_value(literal: &proc_macro2::Literal) -> Option<String> {
string_literal_text(&literal.to_string())
}
pub(crate) fn string_literal_text(repr: &str) -> Option<String> {
if !is_string_literal(repr) {
return None;
}
decode_string_literal(repr, false)
.ok()
.map(|(text, _)| text)
}
fn decode_string_literal(repr: &str, spelling: bool) -> Result<(String, Option<Spelling>), String> {
const BAD: &str = "cannot decode this string literal; expected a plain or raw string literal";
if let Some(rest) = repr.strip_prefix('r') {
let hashes = rest.bytes().take_while(|b| *b == b'#').count();
let body = &rest[hashes..];
if !body.starts_with('"') {
return Err(BAD.to_owned());
}
let inner = &body[1..];
let mut closing = String::with_capacity(1 + hashes);
closing.push('"');
closing.extend(std::iter::repeat_n('#', hashes));
let text = inner
.strip_suffix(&closing)
.ok_or_else(|| BAD.to_owned())?
.to_owned();
let prefix = (repr.len() - inner.len()) as u32;
return Ok((text, spelling.then_some(Spelling::Shift(prefix))));
}
let inner = repr
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.ok_or_else(|| BAD.to_owned())?;
let mut out = String::with_capacity(inner.len());
let mut table: Vec<u32> = Vec::new();
let mut push = |out: &mut String, c: char, at: usize| {
if spelling {
let mut buf = [0u8; 4];
let encoded = c.encode_utf8(&mut buf).len();
table.extend(std::iter::repeat_n(at as u32, encoded));
}
out.push(c);
};
let mut chars = inner.char_indices().peekable();
while let Some((index, c)) = chars.next() {
let at = index + 1;
if c != '\\' {
push(&mut out, c, at);
continue;
}
let Some((_, e)) = chars.next() else {
return Err(BAD.to_owned());
};
match e {
'n' => push(&mut out, '\n', at),
'r' => push(&mut out, '\r', at),
't' => push(&mut out, '\t', at),
'0' => push(&mut out, '\0', at),
'\\' => push(&mut out, '\\', at),
'\'' => push(&mut out, '\'', at),
'"' => push(&mut out, '"', at),
'x' => {
let mut v = 0u32;
for _ in 0..2 {
let Some(d) = chars.next().and_then(|(_, c)| c.to_digit(16)) else {
return Err(BAD.to_owned());
};
v = v * 16 + d;
}
match char::from_u32(v) {
Some(c) => push(&mut out, c, at),
None => return Err(BAD.to_owned()),
}
}
'u' => {
if chars.next().map(|(_, c)| c) != Some('{') {
return Err(BAD.to_owned());
}
let mut v = 0u32;
loop {
match chars.next().map(|(_, c)| c) {
Some('}') => break,
Some('_') => continue,
Some(c) => match c.to_digit(16) {
Some(d) => v = v.saturating_mul(16).saturating_add(d),
None => return Err(BAD.to_owned()),
},
None => return Err(BAD.to_owned()),
}
}
match char::from_u32(v) {
Some(c) => push(&mut out, c, at),
None => return Err(BAD.to_owned()),
}
}
'\n' => {
while matches!(chars.peek(), Some((_, ' ' | '\t' | '\n' | '\r'))) {
chars.next();
}
}
_ => return Err(BAD.to_owned()),
}
}
let spelling = spelling.then(|| {
table.push((repr.len() - 1) as u32);
Spelling::Table(table)
});
Ok((out, spelling))
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum FlatKind {
Ident,
Literal,
Punct,
Delimiter,
}
pub(crate) struct FlatTok {
span: Span,
text: String,
exact: bool,
kind: FlatKind,
joint: bool,
}
impl FlatTok {
fn new(span: Span, fallback: String, kind: FlatKind, joint: bool) -> Self {
match span.source_text() {
Some(text) => FlatTok {
span,
text,
exact: true,
kind,
joint,
},
None => FlatTok {
span,
text: fallback,
exact: false,
kind,
joint,
},
}
}
pub(crate) fn span(&self) -> Span {
self.span
}
pub(crate) fn text(&self) -> &str {
&self.text
}
pub(crate) fn kind(&self) -> FlatKind {
self.kind
}
}
pub(crate) fn shares_previous_span(prev: &FlatTok, tok: &FlatTok) -> bool {
tok.exact && at_same_position(prev.span, tok.span)
}
fn at_same_position(a: Span, b: Span) -> bool {
let (a_start, a_end) = (a.start(), a.end());
let (b_start, b_end) = (b.start(), b.end());
(a_start.line, a_start.column, a_end.line, a_end.column)
== (b_start.line, b_start.column, b_end.line, b_end.column)
}
fn positions_are_usable(toks: &[FlatTok]) -> bool {
let Some(first) = toks.first() else {
return false;
};
toks.iter().any(|t| !at_same_position(first.span, t.span))
}
fn flatten(trees: Vec<TokenTree>, out: &mut Vec<FlatTok>) {
for tt in trees {
match tt {
TokenTree::Group(g) => {
let (open, close) = match g.delimiter() {
Delimiter::Parenthesis => ("(", ")"),
Delimiter::Brace => ("{", "}"),
Delimiter::Bracket => ("[", "]"),
Delimiter::None => {
flatten(g.stream().into_iter().collect(), out);
continue;
}
};
out.push(FlatTok::new(
g.span_open(),
open.to_owned(),
FlatKind::Delimiter,
false,
));
flatten(g.stream().into_iter().collect(), out);
out.push(FlatTok::new(
g.span_close(),
close.to_owned(),
FlatKind::Delimiter,
false,
));
}
TokenTree::Ident(i) => {
out.push(FlatTok::new(
i.span(),
i.to_string(),
FlatKind::Ident,
false,
));
}
TokenTree::Punct(p) => {
let joint = p.spacing() == Spacing::Joint;
out.push(FlatTok::new(
p.span(),
p.as_char().to_string(),
FlatKind::Punct,
joint,
));
}
TokenTree::Literal(l) => {
out.push(FlatTok::new(
l.span(),
l.to_string(),
FlatKind::Literal,
false,
));
}
}
}
}
#[cfg(test)]
pub(crate) fn flat_tokens(input: TokenStream) -> Vec<FlatTok> {
let mut toks = Vec::new();
flatten(input.into_iter().collect(), &mut toks);
toks
}
struct LineIndex {
line_starts: Vec<usize>,
len: usize,
}
impl LineIndex {
fn new(text: &str) -> Self {
let mut line_starts = vec![0usize];
for (i, b) in text.bytes().enumerate() {
if b == b'\n' {
line_starts.push(i + 1);
}
}
Self {
line_starts,
len: text.len(),
}
}
fn offset(&self, text: &str, lc: LineColumn) -> Option<usize> {
let line = lc.line.checked_sub(1)?;
let start = *self.line_starts.get(line)?;
let end = self.line_starts.get(line + 1).copied().unwrap_or(self.len);
let slice = text.get(start..end)?;
let mut count = 0usize;
for (i, _) in slice.char_indices() {
if count == lc.column {
return Some(start + i);
}
count += 1;
}
(count == lc.column).then_some(end)
}
}
struct Located {
path: PathBuf,
text: String,
anchors: AnchorList,
line: usize,
column: usize,
}
impl From<locate::Slice> for Located {
fn from(slice: locate::Slice) -> Self {
Self {
path: slice.path,
text: slice.text,
anchors: slice.anchors,
line: slice.line,
column: slice.column,
}
}
}
fn capture_file_slice(toks: &[FlatTok]) -> Option<Located> {
let first = toks.first()?;
let last = toks.last()?;
let path = first.span.local_file()?;
let content = std::fs::read_to_string(&path).ok()?;
let index = LineIndex::new(&content);
let start = index.offset(&content, first.span.start())?;
let end = index.offset(&content, last.span.end())?;
if end < start || end - start > u32::MAX as usize {
return None;
}
let mut anchors = Vec::with_capacity(toks.len());
let mut prev_end = start;
for t in toks {
let s = index.offset(&content, t.span.start())?;
let e = index.offset(&content, t.span.end())?;
if s < start || e > end || e < s {
return None;
}
let slice = content.get(s..e)?;
if t.exact {
if slice != t.text {
return None;
}
} else {
match t.kind {
FlatKind::Ident | FlatKind::Literal => {
if slice != t.text {
return None;
}
}
FlatKind::Punct | FlatKind::Delimiter => {
if !slice.contains(&t.text) {
return None;
}
}
}
}
if s < prev_end {
continue;
}
prev_end = e;
anchors.push(((s - start) as u32, (e - start) as u32, t.span));
}
let at = first.span.start();
Some(Located {
path,
text: content[start..end].to_owned(),
anchors,
line: at.line,
column: at.column,
})
}
struct Rebuilt {
text: String,
anchors: AnchorList,
blocked: Option<Blocked>,
}
struct Blocked {
span: Span,
message: String,
notes: Vec<String>,
}
fn reconstruct(toks: &[FlatTok]) -> Rebuilt {
let mut out = String::new();
let mut anchors = Vec::new();
let Some(first) = toks.first() else {
return Rebuilt {
text: out,
anchors,
blocked: None,
};
};
let line_base = first.span.start().line;
let col_base = first.span.start().column;
let mut cur_line = 1usize;
let mut cur_col = 0usize;
let mut prev: Option<&FlatTok> = None;
for t in toks {
let s = t.span.start();
if let Some(p) = prev
&& shares_previous_span(p, t)
{
continue;
}
prev = Some(t);
let target_line = s.line.saturating_sub(line_base) + 1;
let target_col = if s.line == line_base {
s.column.saturating_sub(col_base)
} else {
s.column
};
if target_line < cur_line || (target_line == cur_line && target_col < cur_col) {
if !out.is_empty() {
out.push(' ');
cur_col += 1;
}
} else {
while cur_line < target_line {
out.push('\n');
cur_line += 1;
cur_col = 0;
}
while cur_col < target_col {
out.push(' ');
cur_col += 1;
}
}
let anchor_start = out.len();
out.push_str(&t.text);
for ch in t.text.chars() {
if ch == '\n' {
cur_line += 1;
cur_col = 0;
} else {
cur_col += 1;
}
}
anchors.push((anchor_start as u32, out.len() as u32, t.span));
}
Rebuilt {
text: out,
anchors,
blocked: None,
}
}
fn reconstruct_from_tokens(toks: &[FlatTok], entry: &str) -> Rebuilt {
let mut out = Builder::default();
let mut glued = false;
let mut prev: Option<&FlatTok> = None;
let mut i = 0;
while i < toks.len() {
let tok = &toks[i];
if let Some(p) = prev
&& shares_previous_span(p, tok)
{
i += 1;
continue;
}
prev = Some(tok);
if !is_hash(tok) {
out.write(tok, !glued);
glued = tok.kind == FlatKind::Punct && tok.joint;
i += 1;
continue;
}
let rest = &toks[i..];
let Some(shape) = directive_shape(rest) else {
return Rebuilt {
text: String::new(),
anchors: AnchorList::new(),
blocked: Some(blocked_directive(rest, entry)),
};
};
out.newline();
out.write(&rest[0], false);
for (n, tok) in rest[1..shape.tokens()].iter().enumerate() {
out.write(tok, n == 1);
}
out.newline();
glued = false;
i += shape.tokens();
}
Rebuilt {
text: out.text,
anchors: out.anchors,
blocked: None,
}
}
#[derive(Default)]
struct Builder {
text: String,
anchors: AnchorList,
at_line_start: bool,
}
impl Builder {
fn write(&mut self, tok: &FlatTok, space: bool) {
if space && !self.at_line_start && !self.text.is_empty() {
self.text.push(' ');
}
let start = self.text.len() as u32;
self.text.push_str(&tok.text);
self.anchors.push((start, self.text.len() as u32, tok.span));
self.at_line_start = false;
}
fn newline(&mut self) {
if !self.text.is_empty() && !self.at_line_start {
self.text.push('\n');
}
self.at_line_start = true;
}
}
fn is_hash(tok: &FlatTok) -> bool {
tok.kind == FlatKind::Punct && tok.text == "#"
}
#[derive(Clone, Copy)]
enum Shape {
Null,
Bare,
Word,
Quoted,
Angled(usize),
}
impl Shape {
fn tokens(self) -> usize {
match self {
Shape::Null => 1,
Shape::Bare => 2,
Shape::Word | Shape::Quoted => 3,
Shape::Angled(tokens) => 4 + tokens,
}
}
}
fn directive_shape(toks: &[FlatTok]) -> Option<Shape> {
let Some(name) = toks.get(1) else {
return Some(Shape::Null);
};
if name.kind != FlatKind::Ident {
return None;
}
let operand = toks.get(2);
let is_ident = |tok: Option<&FlatTok>, text: &str| {
tok.is_some_and(|t| t.kind == FlatKind::Ident && (text.is_empty() || t.text == text))
};
match name.text.as_str() {
"include" | "include_next" => {
if operand.is_some_and(|t| t.kind == FlatKind::Literal && t.text.starts_with('"')) {
return Some(Shape::Quoted);
}
if operand.is_some_and(|t| t.kind == FlatKind::Punct && t.text == "<") {
let path = toks
.get(3..)?
.iter()
.position(|t| t.kind == FlatKind::Punct && t.text == ">")?;
return Some(Shape::Angled(path));
}
None
}
"ifdef" | "ifndef" | "undef" | "elifdef" | "elifndef" => {
is_ident(operand, "").then_some(Shape::Word)
}
"else" | "endif" => Some(Shape::Bare),
"pragma" => is_ident(operand, "once").then_some(Shape::Word),
_ => None,
}
}
fn blocked_directive(toks: &[FlatTok], entry: &str) -> Blocked {
let what = match toks.get(1) {
Some(name) if name.kind == FlatKind::Ident => format!("the '#{}' directive", name.text),
_ => "this preprocessing directive".to_owned(),
};
Blocked {
span: toks[0].span,
message: format!(
"cannot tell where {what} ends: this block's tokens carry no source positions, so its \
text had to be rebuilt from the tokens alone — and a directive is a line, of which \
tokens keep nothing"
),
notes: vec![
"the compiler that expanded this gave no position for any token, and the invocation \
was not found in this crate's sources either. An editor analysing a file you have \
not saved yet looks exactly like that — rust-analyzer gives a procedural macro no \
positions, and what is on disk no longer matches what you are typing — and so does \
input another macro built."
.to_owned(),
format!(
"save the file and this block is read from disk again, directives and all; or \
write it as a string literal — {entry}! {{ r#\"…\"# }} — which needs no \
positions at all"
),
],
}
}
#[cfg(test)]
mod tests {
use super::*;
fn unescape(repr: &str) -> Result<String, String> {
decode_string_literal(repr, false).map(|(text, _)| text)
}
fn spelling_of(repr: &str, start: u32, end: u32) -> Option<(u32, u32)> {
let (_, spelling) = decode_string_literal(repr, true).unwrap();
spelling.unwrap().range(start, end)
}
#[test]
fn unescape_plain() {
assert_eq!(unescape(r#""a\nb""#).unwrap(), "a\nb");
assert_eq!(unescape(r#""\x41\u{3042}""#).unwrap(), "A\u{3042}");
}
#[test]
fn the_spelling_map_finds_the_bytes_a_character_was_written_as() {
assert_eq!(spelling_of(r#""a\nb""#, 2, 3), Some((4, 5)));
assert_eq!(spelling_of(r#""a\nb""#, 1, 2), Some((2, 4)));
assert_eq!(spelling_of(r####"r##"abc"##"####, 1, 2), Some((5, 6)));
assert_eq!(spelling_of(r#""ab""#, 0, 2), Some((1, 3)));
}
#[test]
fn several_files_share_one_offset_space() {
let mut map = SourceMap::new();
let root = map.add_file(FileSpec {
name: "<input>".to_owned(),
text: "int x;\n".to_owned(),
anchors: Vec::new(),
fallback_span: Span::call_site(),
precise: true,
precise_spans: None,
mode: InputMode::Reconstructed,
rust_path: None,
first_line: 1,
});
let header = map.add_included_file(
"stdio.h",
"int printf();\nint puts();\n".to_owned(),
Span::call_site(),
);
assert_ne!(root, header);
assert_eq!(map.file_of(map.file(root).base()), root);
assert_eq!(map.file_of(map.file(header).base()), header);
assert_eq!(map.file_of(map.file(header).base() + 14), header);
assert_eq!(map.line_col(map.file(header).base() + 14), (2, 1));
assert!(map.is_precise(map.file(root).base()));
assert!(!map.is_precise(map.file(header).base()));
}
#[test]
fn unescape_raw() {
assert_eq!(unescape(r####"r#"a\nb"#"####).unwrap(), r"a\nb");
assert_eq!(unescape(r#"r"x""#).unwrap(), "x");
}
}