blob: 7cc6e3c4e9d902fdc19073917b2ab7334b2ca3a8 [file] [log] [blame]
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2017, Data61, CSIRO (ABN 41 687 119 230)
#
# SPDX-License-Identifier: BSD-2-Clause
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from camkes.internal.seven import cmp, filter, map, zip
import plyplus, re
# The directives that CPP emits to indicate a change of source file and/or line
# number.
LINE_DIRECTIVE = re.compile(
r'\s*#\s*(?:line)?\s*(?P<lineno>\d+)(?:\s+"(?P<filename>[^"]*)")?.*$',
flags=re.UNICODE)
class SourceLocation(object):
'''
The location of a parsed term in its original source file.
This class is primarily useful for showing users useful error messages and
for debugging. Note that "original source" above means the post-processed
source if we are using CPP. This class looks more complicated than you may
have expected because it parses CPP line directives, that are *not*
interpreted by the stage 1 parser, in order to give the user source
locations that match their own interpretation.
'''
def __init__(self, filename, term, full_source):
assert plyplus.is_stree(term) or isinstance(term, plyplus.ParseError)
self._filename = filename
self._lineno = None
self._min_col = None
self._max_col = None
self.term = term
self.full_source = full_source
# We defer narrowing the location because this can be expensive and
# typically goes unused unless we hit a parse error.
self.precise = False
def _locate(self):
'''
Find the exact location we are referencing.
'''
if plyplus.is_stree(self.term):
# First, force plyplus to calculate what it thinks the term's
# location is.
self.term.calc_position()
plyplus_line = self.term.min_line
self._min_col = self.term.min_col
self._max_col = self.term.max_col
else:
assert isinstance(self.term, plyplus.ParseError)
# Try to extract the line number from a plyplus error.
m = re.search(r'^Syntax error in input at \'(?P<token>[^\']*)\' '
r'\(type [^\)]*\) line\s+(?P<line>\d+)'
r'(?:\s+col\s+(?P<col>\d+))?$',
str(self.term), flags=re.MULTILINE)
if m is not None:
plyplus_line = int(m.group('line'))
if m.group('col') is not None:
self._min_col = int(m.group('col'))
if len(m.group('token')) > 0:
self._max_col = self._min_col + len(m.group('token')) \
- 1
else:
plyplus_line = None
if plyplus_line is None:
# We have no opportunity to find a more precise location.
self.precise = True
return
if self.full_source is None:
self._lineno = plyplus_line
self.precise = True
return
# Now parse the original source only looking for CPP line directives,
# to adjust our understanding of the location if necessary.
current_filename = self._filename
current_lineno = 1
for line_index, line in enumerate(self.full_source.split('\n')):
if line_index + 1 == plyplus_line:
# We've reached the line this term is on.
self._filename = current_filename
self._lineno = current_lineno
self.precise = True
return
m = LINE_DIRECTIVE.match(line)
if m is not None:
# The current line is a line directive.
if m.group('filename') is not None:
current_filename = m.group('filename')
current_lineno = int(m.group('lineno'))
else:
# Standard (CAmkES) line.
current_lineno += 1
else:
assert False, \
'term line number points outside its containing source ' \
'(plyplus bug?)'
@property
def filename(self):
if not self.precise:
self._locate()
return self._filename
@property
def lineno(self):
if not self.precise:
self._locate()
return self._lineno
@property
def min_col(self):
if not self.precise:
self._locate()
return self._min_col
@property
def max_col(self):
if not self.precise:
self._locate()
return self._max_col