Skip to content

Commit c5b611a

Browse files
authored
[libclang/python] Expose clang_isBeforeInTranslationUnit for SourceRange.__contains__
Add libclang function `clang_isBeforeInTranslationUnit` to allow checking the order between two source locations. Simplify the `SourceRange.__contains__` implementation using this new function. Add tests for `SourceRange.__contains__` and the newly added functionality. Fixes #22617 Fixes #52827
1 parent c458e9e commit c5b611a

File tree

8 files changed

+146
-22
lines changed

8 files changed

+146
-22
lines changed

clang/bindings/python/clang/cindex.py

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,12 @@ def __eq__(self, other):
319319
def __ne__(self, other):
320320
return not self.__eq__(other)
321321

322+
def __lt__(self, other: SourceLocation) -> bool:
323+
return conf.lib.clang_isBeforeInTranslationUnit(self, other) # type: ignore [no-any-return]
324+
325+
def __le__(self, other: SourceLocation) -> bool:
326+
return self < other or self == other
327+
322328
def __repr__(self):
323329
if self.file:
324330
filename = self.file.name
@@ -375,26 +381,7 @@ def __contains__(self, other):
375381
"""Useful to detect the Token/Lexer bug"""
376382
if not isinstance(other, SourceLocation):
377383
return False
378-
if other.file is None and self.start.file is None:
379-
pass
380-
elif (
381-
self.start.file.name != other.file.name
382-
or other.file.name != self.end.file.name
383-
):
384-
# same file name
385-
return False
386-
# same file, in between lines
387-
if self.start.line < other.line < self.end.line:
388-
return True
389-
elif self.start.line == other.line:
390-
# same file first line
391-
if self.start.column <= other.column:
392-
return True
393-
elif other.line == self.end.line:
394-
# same file last line
395-
if other.column <= self.end.column:
396-
return True
397-
return False
384+
return self.start <= other <= self.end
398385

399386
def __repr__(self):
400387
return "<SourceRange start %r, end %r>" % (self.start, self.end)
@@ -3908,6 +3895,7 @@ def write_main_file_to_stdout(self):
39083895
("clang_isUnexposed", [CursorKind], bool),
39093896
("clang_isVirtualBase", [Cursor], bool),
39103897
("clang_isVolatileQualifiedType", [Type], bool),
3898+
("clang_isBeforeInTranslationUnit", [SourceLocation, SourceLocation], bool),
39113899
(
39123900
"clang_parseTranslationUnit",
39133901
[Index, c_interop_string, c_void_p, c_int, c_void_p, c_int, c_int],

clang/bindings/python/tests/cindex/test_location.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,23 @@ def test_is_system_location(self):
130130
two = get_cursor(tu, "two")
131131
self.assertFalse(one.location.is_in_system_header)
132132
self.assertTrue(two.location.is_in_system_header)
133+
134+
def test_operator_lt(self):
135+
tu = get_tu("aaaaa")
136+
t1_f = tu.get_file(tu.spelling)
137+
tu2 = get_tu("aaaaa")
138+
139+
l_t1_12 = SourceLocation.from_position(tu, t1_f, 1, 2)
140+
l_t1_13 = SourceLocation.from_position(tu, t1_f, 1, 3)
141+
l_t1_14 = SourceLocation.from_position(tu, t1_f, 1, 4)
142+
143+
l_t2_13 = SourceLocation.from_position(tu2, tu2.get_file(tu2.spelling), 1, 3)
144+
145+
# In same file
146+
assert l_t1_12 < l_t1_13 < l_t1_14
147+
assert not l_t1_13 < l_t1_12
148+
149+
# In same file, different TU
150+
assert l_t1_12 < l_t2_13 < l_t1_14
151+
assert not l_t2_13 < l_t1_12
152+
assert not l_t1_14 < l_t2_13
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import os
2+
from clang.cindex import Config
3+
4+
if "CLANG_LIBRARY_PATH" in os.environ:
5+
Config.set_library_path(os.environ["CLANG_LIBRARY_PATH"])
6+
7+
import unittest
8+
from clang.cindex import SourceLocation, SourceRange, TranslationUnit
9+
10+
from .util import get_tu
11+
12+
13+
def create_range(tu, line1, column1, line2, column2):
14+
return SourceRange.from_locations(
15+
SourceLocation.from_position(tu, tu.get_file(tu.spelling), line1, column1),
16+
SourceLocation.from_position(tu, tu.get_file(tu.spelling), line2, column2),
17+
)
18+
19+
20+
class TestSourceRange(unittest.TestCase):
21+
def test_contains(self):
22+
tu = get_tu(
23+
"""aaaaa
24+
aaaaa
25+
aaaaa
26+
aaaaa"""
27+
)
28+
file = tu.get_file(tu.spelling)
29+
30+
l13 = SourceLocation.from_position(tu, file, 1, 3)
31+
l21 = SourceLocation.from_position(tu, file, 2, 1)
32+
l22 = SourceLocation.from_position(tu, file, 2, 2)
33+
l23 = SourceLocation.from_position(tu, file, 2, 3)
34+
l24 = SourceLocation.from_position(tu, file, 2, 4)
35+
l25 = SourceLocation.from_position(tu, file, 2, 5)
36+
l33 = SourceLocation.from_position(tu, file, 3, 3)
37+
l31 = SourceLocation.from_position(tu, file, 3, 1)
38+
r22_24 = create_range(tu, 2, 2, 2, 4)
39+
r23_23 = create_range(tu, 2, 3, 2, 3)
40+
r24_32 = create_range(tu, 2, 4, 3, 2)
41+
r14_32 = create_range(tu, 1, 4, 3, 2)
42+
43+
assert l13 not in r22_24 # Line before start
44+
assert l21 not in r22_24 # Column before start
45+
assert l22 in r22_24 # Colum on start
46+
assert l23 in r22_24 # Column in range
47+
assert l24 in r22_24 # Column on end
48+
assert l25 not in r22_24 # Column after end
49+
assert l33 not in r22_24 # Line after end
50+
51+
assert l23 in r23_23 # In one-column range
52+
53+
assert l23 not in r24_32 # Outside range in first line
54+
assert l33 not in r24_32 # Outside range in last line
55+
assert l25 in r24_32 # In range in first line
56+
assert l31 in r24_32 # In range in last line
57+
58+
assert l21 in r14_32 # In range at start of center line
59+
assert l25 in r14_32 # In range at end of center line
60+
61+
# In range within included file
62+
tu2 = TranslationUnit.from_source(
63+
"main.c",
64+
unsaved_files=[
65+
(
66+
"main.c",
67+
"""int a[] = {
68+
#include "numbers.inc"
69+
};
70+
""",
71+
),
72+
(
73+
"./numbers.inc",
74+
"""1,
75+
2,
76+
3,
77+
4
78+
""",
79+
),
80+
],
81+
)
82+
83+
r_curly = create_range(tu2, 1, 11, 3, 1)
84+
l_f2 = SourceLocation.from_position(tu2, tu2.get_file("./numbers.inc"), 4, 1)
85+
assert l_f2 in r_curly

clang/docs/ReleaseNotes.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ Clang Python Bindings Potentially Breaking Changes
9090
- Calling a property on the ``CompletionChunk`` or ``CompletionString`` class
9191
statically now leads to an error, instead of returning a ``CachedProperty`` object
9292
that is used internally. Properties are only available on instances.
93+
- For a single-line ``SourceRange`` and a ``SourceLocation`` in the same line,
94+
but after the end of the ``SourceRange``, ``SourceRange.__contains__``
95+
used to incorrectly return ``True``. (#GH22617), (#GH52827)
9396

9497
What's New in Clang |release|?
9598
==============================
@@ -364,6 +367,8 @@ clang-format
364367

365368
libclang
366369
--------
370+
- Add ``clang_isBeforeInTranslationUnit``. Given two source locations, it determines
371+
whether the first one comes strictly before the second in the source code.
367372

368373
Static Analyzer
369374
---------------

clang/include/clang-c/CXSourceLocation.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ CINDEX_LINKAGE CXSourceLocation clang_getNullLocation(void);
7474
CINDEX_LINKAGE unsigned clang_equalLocations(CXSourceLocation loc1,
7575
CXSourceLocation loc2);
7676

77+
/**
78+
* Determine for two source locations if the first comes
79+
* strictly before the second one in the source code.
80+
*
81+
* \returns non-zero if the first source location comes
82+
* strictly before the second one, zero otherwise.
83+
*/
84+
CINDEX_LINKAGE unsigned clang_isBeforeInTranslationUnit(CXSourceLocation loc1,
85+
CXSourceLocation loc2);
86+
7787
/**
7888
* Returns non-zero if the given source location is in a system header.
7989
*/

clang/lib/Basic/SourceManager.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,8 +2038,7 @@ bool SourceManager::isBeforeInTranslationUnit(SourceLocation LHS,
20382038
std::pair<bool, bool> InSameTU = isInTheSameTranslationUnit(LOffs, ROffs);
20392039
if (InSameTU.first)
20402040
return InSameTU.second;
2041-
// TODO: This should be unreachable, but some clients are calling this
2042-
// function before making sure LHS and RHS are in the same TU.
2041+
// This case is used by libclang: clang_isBeforeInTranslationUnit
20432042
return LOffs.first < ROffs.first;
20442043
}
20452044

clang/tools/libclang/CXSourceLocation.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ unsigned clang_equalLocations(CXSourceLocation loc1, CXSourceLocation loc2) {
5050
loc1.int_data == loc2.int_data);
5151
}
5252

53+
unsigned clang_isBeforeInTranslationUnit(CXSourceLocation loc1,
54+
CXSourceLocation loc2) {
55+
const SourceLocation Loc1 = SourceLocation::getFromRawEncoding(loc1.int_data);
56+
const SourceLocation Loc2 = SourceLocation::getFromRawEncoding(loc2.int_data);
57+
58+
const SourceManager &SM =
59+
*static_cast<const SourceManager *>(loc1.ptr_data[0]);
60+
// Use the appropriate SourceManager method here rather than operator< because
61+
// ordering is meaningful only if LHS and RHS have the same FileID.
62+
return SM.isBeforeInTranslationUnit(Loc1, Loc2);
63+
}
64+
5365
CXSourceRange clang_getNullRange() {
5466
CXSourceRange Result = { { nullptr, nullptr }, 0, 0 };
5567
return Result;

clang/tools/libclang/libclang.map

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,11 @@ LLVM_19 {
434434
clang_Cursor_getBinaryOpcodeStr;
435435
};
436436

437+
LLVM_20 {
438+
global:
439+
clang_isBeforeInTranslationUnit;
440+
};
441+
437442
# Example of how to add a new symbol version entry. If you do add a new symbol
438443
# version, please update the example to depend on the version you added.
439444
# LLVM_X {

0 commit comments

Comments
 (0)