forked from OSchip/llvm-project
cindex/Python: Add full support for Diagnostic and FixIt objects, available via TranslationUnit.diagnostics.
Several important FIXMEs remain: - We aren't getting all the notes? - There is still no way to get diagnostics for invalid inputs. llvm-svn: 94933
This commit is contained in:
parent
15635b8f5c
commit
a7a354e3e3
|
@ -48,6 +48,10 @@ call is efficient.
|
|||
# TODO
|
||||
# ====
|
||||
#
|
||||
# o API support for invalid translation units. Currently we can't even get the
|
||||
# diagnostics on failure because they refer to locations in an object that
|
||||
# will have been invalidated.
|
||||
#
|
||||
# o fix memory management issues (currently client must hold on to index and
|
||||
# translation unit, or risk crashes).
|
||||
#
|
||||
|
@ -145,6 +149,12 @@ class SourceRange(Structure):
|
|||
("begin_int_data", c_uint),
|
||||
("end_int_data", c_uint)]
|
||||
|
||||
# FIXME: Eliminate this and make normal constructor? Requires hiding ctypes
|
||||
# object.
|
||||
@staticmethod
|
||||
def from_locations(start, end):
|
||||
return SourceRange_getRange(start, end)
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
"""
|
||||
|
@ -164,6 +174,44 @@ class SourceRange(Structure):
|
|||
def __repr__(self):
|
||||
return "<SourceRange start %r, end %r>" % (self.start, self.end)
|
||||
|
||||
class Diagnostic(object):
|
||||
"""
|
||||
A Diagnostic is a single instance of a Clang diagnostic. It includes the
|
||||
diagnostic severity, the message, the location the diagnostic occurred, as
|
||||
well as additional source ranges and associated fix-it hints.
|
||||
"""
|
||||
|
||||
Ignored = 0
|
||||
Note = 1
|
||||
Warning = 2
|
||||
Error = 3
|
||||
Fatal = 4
|
||||
|
||||
def __init__(self, severity, location, spelling, ranges, fixits):
|
||||
self.severity = severity
|
||||
self.location = location
|
||||
self.spelling = spelling
|
||||
self.ranges = ranges
|
||||
self.fixits = fixits
|
||||
|
||||
def __repr__(self):
|
||||
return "<Diagnostic severity %r, location %r, spelling %r>" % (
|
||||
self.severity, self.location, self.spelling)
|
||||
|
||||
class FixIt(object):
|
||||
"""
|
||||
A FixIt represents a transformation to be applied to the source to
|
||||
"fix-it". The fix-it shouldbe applied by replacing the given source range
|
||||
with the given value.
|
||||
"""
|
||||
|
||||
def __init__(self, range, value):
|
||||
self.range = range
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return "<FixIt range %r, value %r>" % (self.range, self.value)
|
||||
|
||||
### Cursor Kinds ###
|
||||
|
||||
class CursorKind(object):
|
||||
|
@ -455,7 +503,7 @@ class Cursor(Structure):
|
|||
children.append(child)
|
||||
return 1 # continue
|
||||
children = []
|
||||
Cursor_visit(self, Callback(visitor), children)
|
||||
Cursor_visit(self, Cursor_visit_callback(visitor), children)
|
||||
return iter(children)
|
||||
|
||||
@staticmethod
|
||||
|
@ -489,6 +537,109 @@ class _CXUnsavedFile(Structure):
|
|||
"""Helper for passing unsaved file arguments."""
|
||||
_fields_ = [("name", c_char_p), ("contents", c_char_p), ('length', c_ulong)]
|
||||
|
||||
## Diagnostic Conversion ##
|
||||
|
||||
# Diagnostic objects are temporary, we must extract all the information from the
|
||||
# diagnostic object when it is passed to the callback.
|
||||
|
||||
_clang_getDiagnosticSeverity = lib.clang_getDiagnosticSeverity
|
||||
_clang_getDiagnosticSeverity.argtypes = [c_object_p]
|
||||
_clang_getDiagnosticSeverity.restype = c_int
|
||||
|
||||
_clang_getDiagnosticLocation = lib.clang_getDiagnosticLocation
|
||||
_clang_getDiagnosticLocation.argtypes = [c_object_p]
|
||||
_clang_getDiagnosticLocation.restype = SourceLocation
|
||||
|
||||
_clang_getDiagnosticSpelling = lib.clang_getDiagnosticSpelling
|
||||
_clang_getDiagnosticSpelling.argtypes = [c_object_p]
|
||||
_clang_getDiagnosticSpelling.restype = _CXString
|
||||
_clang_getDiagnosticSpelling.errcheck = _CXString.from_result
|
||||
|
||||
_clang_getDiagnosticRanges = lib.clang_getDiagnosticRanges
|
||||
_clang_getDiagnosticRanges.argtypes = [c_object_p,
|
||||
POINTER(POINTER(SourceRange)),
|
||||
POINTER(c_uint)]
|
||||
_clang_getDiagnosticRanges.restype = None
|
||||
|
||||
_clang_disposeDiagnosticRanges = lib.clang_disposeDiagnosticRanges
|
||||
_clang_disposeDiagnosticRanges.argtypes = [POINTER(SourceRange), c_uint]
|
||||
_clang_disposeDiagnosticRanges.restype = None
|
||||
|
||||
_clang_getDiagnosticNumFixIts = lib.clang_getDiagnosticNumFixIts
|
||||
_clang_getDiagnosticNumFixIts.argtypes = [c_object_p]
|
||||
_clang_getDiagnosticNumFixIts.restype = c_uint
|
||||
|
||||
_clang_getDiagnosticFixItKind = lib.clang_getDiagnosticFixItKind
|
||||
_clang_getDiagnosticFixItKind.argtypes = [c_object_p, c_uint]
|
||||
_clang_getDiagnosticFixItKind.restype = c_int
|
||||
|
||||
_clang_getDiagnosticFixItInsertion = lib.clang_getDiagnosticFixItInsertion
|
||||
_clang_getDiagnosticFixItInsertion.argtypes = [c_object_p, c_uint,
|
||||
POINTER(SourceLocation)]
|
||||
_clang_getDiagnosticFixItInsertion.restype = _CXString
|
||||
_clang_getDiagnosticFixItInsertion.errcheck = _CXString.from_result
|
||||
|
||||
_clang_getDiagnosticFixItRemoval = lib.clang_getDiagnosticFixItRemoval
|
||||
_clang_getDiagnosticFixItRemoval.argtypes = [c_object_p, c_uint,
|
||||
POINTER(SourceLocation)]
|
||||
_clang_getDiagnosticFixItRemoval.restype = _CXString
|
||||
_clang_getDiagnosticFixItRemoval.errcheck = _CXString.from_result
|
||||
|
||||
_clang_getDiagnosticFixItReplacement = lib.clang_getDiagnosticFixItReplacement
|
||||
_clang_getDiagnosticFixItReplacement.argtypes = [c_object_p, c_uint,
|
||||
POINTER(SourceRange)]
|
||||
_clang_getDiagnosticFixItReplacement.restype = _CXString
|
||||
_clang_getDiagnosticFixItReplacement.errcheck = _CXString.from_result
|
||||
|
||||
def _convert_fixit(diag_ptr, index):
|
||||
# We normalize all the fix-its to a single representation, this is more
|
||||
# convenient.
|
||||
#
|
||||
# FIXME: Push this back into API? It isn't exactly clear what the
|
||||
# SourceRange semantics are, we should make sure we can represent an empty
|
||||
# range.
|
||||
kind = _clang_getDiagnosticFixItKind(diag_ptr, index)
|
||||
range = None
|
||||
value = None
|
||||
if kind == 0: # insertion
|
||||
location = SourceLocation()
|
||||
value = _clang_getDiagnosticFixItInsertion(diag_ptr, index,
|
||||
byref(location))
|
||||
range = SourceRange.from_locations(location, location)
|
||||
elif kind == 1: # removal
|
||||
range = _clang_getDiagnosticFixItRemoval(diag_ptr, index)
|
||||
value = ''
|
||||
else: # replacement
|
||||
assert kind == 2
|
||||
range = SourceRange()
|
||||
value = _clang_getDiagnosticFixItReplacement(diag_ptr, index,
|
||||
byref(range))
|
||||
return FixIt(range, value)
|
||||
|
||||
def _convert_diag(diag_ptr, diag_list):
|
||||
severity = _clang_getDiagnosticSeverity(diag_ptr)
|
||||
loc = _clang_getDiagnosticLocation(diag_ptr)
|
||||
spelling = _clang_getDiagnosticSpelling(diag_ptr)
|
||||
|
||||
# Diagnostic ranges.
|
||||
#
|
||||
# FIXME: Use getNum... based API?
|
||||
num_ranges = c_uint()
|
||||
ranges_array = POINTER(SourceRange)()
|
||||
_clang_getDiagnosticRanges(diag_ptr, byref(ranges_array), byref(num_ranges))
|
||||
|
||||
# Copy the ranges array so we can dispose the original.
|
||||
ranges = [SourceRange.from_buffer_copy(ranges_array[i])
|
||||
for i in range(num_ranges.value)]
|
||||
_clang_disposeDiagnosticRanges(ranges_array, num_ranges)
|
||||
|
||||
fixits = [_convert_fixit(diag_ptr, i)
|
||||
for i in range(_clang_getDiagnosticNumFixIts(diag_ptr))]
|
||||
|
||||
diag_list.append(Diagnostic(severity, loc, spelling, ranges, fixits))
|
||||
|
||||
###
|
||||
|
||||
class Index(ClangObject):
|
||||
"""
|
||||
The Index type provides the primary interface to the Clang CIndex library,
|
||||
|
@ -510,7 +661,11 @@ class Index(ClangObject):
|
|||
|
||||
def read(self, path):
|
||||
"""Load the translation unit from the given AST file."""
|
||||
ptr = TranslationUnit_read(self, path)
|
||||
# FIXME: In theory, we could support streaming diagnostics. It's hard to
|
||||
# integrate this into the API cleanly, however. Resolve.
|
||||
diags = []
|
||||
ptr = TranslationUnit_read(self, path,
|
||||
Diagnostic_callback(_convert_diag), diags)
|
||||
return TranslationUnit(ptr) if ptr else None
|
||||
|
||||
def parse(self, path, args = [], unsaved_files = []):
|
||||
|
@ -541,9 +696,13 @@ class Index(ClangObject):
|
|||
unsaved_files_array[i].name = name
|
||||
unsaved_files_array[i].contents = value
|
||||
unsaved_files_array[i].length = len(value)
|
||||
# FIXME: In theory, we could support streaming diagnostics. It's hard to
|
||||
# integrate this into the API cleanly, however. Resolve.
|
||||
diags = []
|
||||
ptr = TranslationUnit_parse(self, path, len(args), arg_array,
|
||||
len(unsaved_files), unsaved_files_array)
|
||||
return TranslationUnit(ptr) if ptr else None
|
||||
len(unsaved_files), unsaved_files_array,
|
||||
Diagnostic_callback(_convert_diag), diags)
|
||||
return TranslationUnit(ptr, diags) if ptr else None
|
||||
|
||||
|
||||
class TranslationUnit(ClangObject):
|
||||
|
@ -552,6 +711,10 @@ class TranslationUnit(ClangObject):
|
|||
provides read-only access to its top-level declarations.
|
||||
"""
|
||||
|
||||
def __init__(self, ptr, diagnostics):
|
||||
ClangObject.__init__(self, ptr)
|
||||
self.diagnostics = diagnostics
|
||||
|
||||
def __del__(self):
|
||||
TranslationUnit_dispose(self)
|
||||
|
||||
|
@ -583,9 +746,6 @@ class File(ClangObject):
|
|||
|
||||
# Additional Functions and Types
|
||||
|
||||
# Wrap calls to TranslationUnit._load and Decl._load.
|
||||
Callback = CFUNCTYPE(c_int, Cursor, Cursor, py_object)
|
||||
|
||||
# String Functions
|
||||
_CXString_dispose = lib.clang_disposeString
|
||||
_CXString_dispose.argtypes = [_CXString]
|
||||
|
@ -601,6 +761,10 @@ SourceLocation_loc.argtypes = [SourceLocation, POINTER(c_object_p),
|
|||
POINTER(c_uint)]
|
||||
|
||||
# Source Range Functions
|
||||
SourceRange_getRange = lib.clang_getRange
|
||||
SourceRange_getRange.argtypes = [SourceLocation, SourceLocation]
|
||||
SourceRange_getRange.restype = SourceRange
|
||||
|
||||
SourceRange_start = lib.clang_getRangeStart
|
||||
SourceRange_start.argtypes = [SourceRange]
|
||||
SourceRange_start.restype = SourceLocation
|
||||
|
@ -675,8 +839,9 @@ Cursor_ref.argtypes = [Cursor]
|
|||
Cursor_ref.restype = Cursor
|
||||
Cursor_ref.errcheck = Cursor.from_result
|
||||
|
||||
Cursor_visit_callback = CFUNCTYPE(c_int, Cursor, Cursor, py_object)
|
||||
Cursor_visit = lib.clang_visitChildren
|
||||
Cursor_visit.argtypes = [Cursor, Callback, py_object]
|
||||
Cursor_visit.argtypes = [Cursor, Cursor_visit_callback, py_object]
|
||||
Cursor_visit.restype = c_uint
|
||||
|
||||
# Index Functions
|
||||
|
@ -688,13 +853,17 @@ Index_dispose = lib.clang_disposeIndex
|
|||
Index_dispose.argtypes = [Index]
|
||||
|
||||
# Translation Unit Functions
|
||||
Diagnostic_callback = CFUNCTYPE(None, c_object_p, py_object)
|
||||
|
||||
TranslationUnit_read = lib.clang_createTranslationUnit
|
||||
TranslationUnit_read.argtypes = [Index, c_char_p]
|
||||
TranslationUnit_read.argtypes = [Index, c_char_p,
|
||||
Diagnostic_callback, py_object]
|
||||
TranslationUnit_read.restype = c_object_p
|
||||
|
||||
TranslationUnit_parse = lib.clang_createTranslationUnitFromSourceFile
|
||||
TranslationUnit_parse.argtypes = [Index, c_char_p, c_int, c_void_p,
|
||||
c_int, c_void_p]
|
||||
c_int, c_void_p,
|
||||
Diagnostic_callback, py_object]
|
||||
TranslationUnit_parse.restype = c_object_p
|
||||
|
||||
TranslationUnit_cursor = lib.clang_getTranslationUnitCursor
|
||||
|
@ -722,4 +891,4 @@ File_time.restype = c_uint
|
|||
###
|
||||
|
||||
__all__ = ['Index', 'TranslationUnit', 'Cursor', 'CursorKind',
|
||||
'SourceRange', 'SourceLocation', 'File']
|
||||
'Diagnostic', 'FixIt', 'SourceRange', 'SourceLocation', 'File']
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
from clang.cindex import *
|
||||
|
||||
def tu_from_source(source):
|
||||
index = Index.create()
|
||||
tu = index.parse('INPUT.c', unsaved_files = [('INPUT.c', source)])
|
||||
# FIXME: Remove the need for this.
|
||||
tu.index = index
|
||||
return tu
|
||||
|
||||
# FIXME: We need support for invalid translation units to test better.
|
||||
|
||||
def test_diagnostic_warning():
|
||||
tu = tu_from_source("""int f0() {}\n""")
|
||||
assert len(tu.diagnostics) == 1
|
||||
assert tu.diagnostics[0].severity == Diagnostic.Warning
|
||||
assert tu.diagnostics[0].location.line == 1
|
||||
assert tu.diagnostics[0].location.column == 11
|
||||
assert (tu.diagnostics[0].spelling ==
|
||||
'control reaches end of non-void function')
|
||||
|
||||
def test_diagnostic_note():
|
||||
# FIXME: We aren't getting notes here for some reason.
|
||||
index = Index.create()
|
||||
tu = tu_from_source("""#define A x\nvoid *A = 1;\n""")
|
||||
assert len(tu.diagnostics) == 1
|
||||
assert tu.diagnostics[0].severity == Diagnostic.Warning
|
||||
assert tu.diagnostics[0].location.line == 2
|
||||
assert tu.diagnostics[0].location.column == 7
|
||||
assert 'incompatible' in tu.diagnostics[0].spelling
|
||||
# assert tu.diagnostics[1].severity == Diagnostic.Note
|
||||
# assert tu.diagnostics[1].location.line == 1
|
||||
# assert tu.diagnostics[1].location.column == 11
|
||||
# assert tu.diagnostics[1].spelling == 'instantiated from'
|
||||
|
||||
def test_diagnostic_fixit():
|
||||
index = Index.create()
|
||||
tu = tu_from_source("""struct { int f0; } x = { f0 : 1 };""")
|
||||
assert len(tu.diagnostics) == 1
|
||||
assert tu.diagnostics[0].severity == Diagnostic.Warning
|
||||
assert tu.diagnostics[0].location.line == 1
|
||||
assert tu.diagnostics[0].location.column == 31
|
||||
assert tu.diagnostics[0].spelling.startswith('use of GNU old-style')
|
||||
assert len(tu.diagnostics[0].fixits) == 1
|
||||
assert tu.diagnostics[0].fixits[0].range.start.line == 1
|
||||
assert tu.diagnostics[0].fixits[0].range.start.column == 26
|
||||
assert tu.diagnostics[0].fixits[0].range.end.line == 1
|
||||
assert tu.diagnostics[0].fixits[0].range.end.column == 29
|
||||
assert tu.diagnostics[0].fixits[0].value == '.f0 = '
|
Loading…
Reference in New Issue