1 | //=== unittests/Sema/CodeCompleteTest.cpp - Code Complete tests ==============// |
2 | // |
3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | // See https://llvm.org/LICENSE.txt for license information. |
5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | |
9 | #include "clang/Frontend/CompilerInstance.h" |
10 | #include "clang/Frontend/FrontendActions.h" |
11 | #include "clang/Lex/Preprocessor.h" |
12 | #include "clang/Parse/ParseAST.h" |
13 | #include "clang/Sema/Sema.h" |
14 | #include "clang/Sema/SemaDiagnostic.h" |
15 | #include "clang/Tooling/Tooling.h" |
16 | #include "llvm/Testing/Annotations/Annotations.h" |
17 | #include "gmock/gmock.h" |
18 | #include "gtest/gtest.h" |
19 | #include <cstddef> |
20 | #include <string> |
21 | |
22 | namespace { |
23 | |
24 | using namespace clang; |
25 | using namespace clang::tooling; |
26 | using ::testing::AllOf; |
27 | using ::testing::Contains; |
28 | using ::testing::Each; |
29 | using ::testing::UnorderedElementsAre; |
30 | |
31 | const char TestCCName[] = "test.cc" ; |
32 | |
33 | struct CompletionContext { |
34 | std::vector<std::string> VisitedNamespaces; |
35 | std::string PreferredType; |
36 | // String representation of std::ptrdiff_t on a given platform. This is a hack |
37 | // to properly account for different configurations of clang. |
38 | std::string PtrDiffType; |
39 | }; |
40 | |
41 | struct CompletedFunctionDecl { |
42 | std::string Name; |
43 | bool IsStatic; |
44 | bool CanBeCall; |
45 | }; |
46 | MATCHER_P(named, name, "" ) { return arg.Name == name; } |
47 | MATCHER_P(isStatic, value, "" ) { return arg.IsStatic == value; } |
48 | MATCHER_P(canBeCall, value, "" ) { return arg.CanBeCall == value; } |
49 | |
50 | class SaveCompletedFunctions : public CodeCompleteConsumer { |
51 | public: |
52 | SaveCompletedFunctions(std::vector<CompletedFunctionDecl> &CompletedFuncDecls) |
53 | : CodeCompleteConsumer(/*CodeCompleteOpts=*/{}), |
54 | CompletedFuncDecls(CompletedFuncDecls), |
55 | CCTUInfo(std::make_shared<GlobalCodeCompletionAllocator>()) {} |
56 | |
57 | void ProcessCodeCompleteResults(Sema &S, CodeCompletionContext Context, |
58 | CodeCompletionResult *Results, |
59 | unsigned NumResults) override { |
60 | for (unsigned I = 0; I < NumResults; ++I) { |
61 | auto R = Results[I]; |
62 | if (R.Kind == CodeCompletionResult::RK_Declaration) { |
63 | if (const auto *FD = llvm::dyn_cast<FunctionDecl>(R.getDeclaration())) { |
64 | CompletedFunctionDecl D; |
65 | D.Name = FD->getNameAsString(); |
66 | D.CanBeCall = R.FunctionCanBeCall; |
67 | D.IsStatic = FD->isStatic(); |
68 | CompletedFuncDecls.emplace_back(std::move(D)); |
69 | } |
70 | } |
71 | } |
72 | } |
73 | |
74 | private: |
75 | CodeCompletionAllocator &getAllocator() override { |
76 | return CCTUInfo.getAllocator(); |
77 | } |
78 | |
79 | CodeCompletionTUInfo &getCodeCompletionTUInfo() override { return CCTUInfo; } |
80 | |
81 | std::vector<CompletedFunctionDecl> &CompletedFuncDecls; |
82 | |
83 | CodeCompletionTUInfo CCTUInfo; |
84 | }; |
85 | |
86 | class VisitedContextFinder : public CodeCompleteConsumer { |
87 | public: |
88 | VisitedContextFinder(CompletionContext &ResultCtx) |
89 | : CodeCompleteConsumer(/*CodeCompleteOpts=*/{}), ResultCtx(ResultCtx), |
90 | CCTUInfo(std::make_shared<GlobalCodeCompletionAllocator>()) {} |
91 | |
92 | void ProcessCodeCompleteResults(Sema &S, CodeCompletionContext Context, |
93 | CodeCompletionResult *Results, |
94 | unsigned NumResults) override { |
95 | ResultCtx.VisitedNamespaces = |
96 | getVisitedNamespace(Context.getVisitedContexts()); |
97 | ResultCtx.PreferredType = Context.getPreferredType().getAsString(); |
98 | ResultCtx.PtrDiffType = |
99 | S.getASTContext().getPointerDiffType().getAsString(); |
100 | } |
101 | |
102 | CodeCompletionAllocator &getAllocator() override { |
103 | return CCTUInfo.getAllocator(); |
104 | } |
105 | |
106 | CodeCompletionTUInfo &getCodeCompletionTUInfo() override { return CCTUInfo; } |
107 | |
108 | private: |
109 | std::vector<std::string> getVisitedNamespace( |
110 | CodeCompletionContext::VisitedContextSet VisitedContexts) const { |
111 | std::vector<std::string> NSNames; |
112 | for (const auto *Context : VisitedContexts) |
113 | if (const auto *NS = llvm::dyn_cast<NamespaceDecl>(Context)) |
114 | NSNames.push_back(NS->getQualifiedNameAsString()); |
115 | return NSNames; |
116 | } |
117 | |
118 | CompletionContext &ResultCtx; |
119 | CodeCompletionTUInfo CCTUInfo; |
120 | }; |
121 | |
122 | class CodeCompleteAction : public SyntaxOnlyAction { |
123 | public: |
124 | CodeCompleteAction(ParsedSourceLocation P, CodeCompleteConsumer *Consumer) |
125 | : CompletePosition(std::move(P)), Consumer(Consumer) {} |
126 | |
127 | bool BeginInvocation(CompilerInstance &CI) override { |
128 | CI.getFrontendOpts().CodeCompletionAt = CompletePosition; |
129 | CI.setCodeCompletionConsumer(Consumer); |
130 | return true; |
131 | } |
132 | |
133 | private: |
134 | // 1-based code complete position <Line, Col>; |
135 | ParsedSourceLocation CompletePosition; |
136 | CodeCompleteConsumer *Consumer; |
137 | }; |
138 | |
139 | ParsedSourceLocation offsetToPosition(llvm::StringRef Code, size_t Offset) { |
140 | Offset = std::min(Code.size(), Offset); |
141 | StringRef Before = Code.substr(0, Offset); |
142 | int Lines = Before.count('\n'); |
143 | size_t PrevNL = Before.rfind('\n'); |
144 | size_t StartOfLine = (PrevNL == StringRef::npos) ? 0 : (PrevNL + 1); |
145 | return {TestCCName, static_cast<unsigned>(Lines + 1), |
146 | static_cast<unsigned>(Offset - StartOfLine + 1)}; |
147 | } |
148 | |
149 | CompletionContext runCompletion(StringRef Code, size_t Offset) { |
150 | CompletionContext ResultCtx; |
151 | clang::tooling::runToolOnCodeWithArgs( |
152 | std::make_unique<CodeCompleteAction>(offsetToPosition(Code, Offset), |
153 | new VisitedContextFinder(ResultCtx)), |
154 | Code, {"-std=c++11" }, TestCCName); |
155 | return ResultCtx; |
156 | } |
157 | |
158 | CompletionContext runCodeCompleteOnCode(StringRef AnnotatedCode) { |
159 | llvm::Annotations A(AnnotatedCode); |
160 | return runCompletion(A.code(), A.point()); |
161 | } |
162 | |
163 | std::vector<std::string> |
164 | collectPreferredTypes(StringRef AnnotatedCode, |
165 | std::string *PtrDiffType = nullptr) { |
166 | llvm::Annotations A(AnnotatedCode); |
167 | std::vector<std::string> Types; |
168 | for (size_t Point : A.points()) { |
169 | auto Results = runCompletion(A.code(), Point); |
170 | if (PtrDiffType) { |
171 | assert(PtrDiffType->empty() || *PtrDiffType == Results.PtrDiffType); |
172 | *PtrDiffType = Results.PtrDiffType; |
173 | } |
174 | Types.push_back(Results.PreferredType); |
175 | } |
176 | return Types; |
177 | } |
178 | |
179 | std::vector<CompletedFunctionDecl> |
180 | CollectCompletedFunctions(StringRef Code, std::size_t Point) { |
181 | std::vector<CompletedFunctionDecl> Result; |
182 | clang::tooling::runToolOnCodeWithArgs( |
183 | std::make_unique<CodeCompleteAction>(offsetToPosition(Code, Point), |
184 | new SaveCompletedFunctions(Result)), |
185 | Code, {"-std=c++11" }, TestCCName); |
186 | return Result; |
187 | } |
188 | |
189 | TEST(SemaCodeCompleteTest, FunctionCanBeCall) { |
190 | llvm::Annotations Code(R"cpp( |
191 | struct Foo { |
192 | static int staticMethod(); |
193 | int method() const; |
194 | Foo() { |
195 | this->$canBeCall^ |
196 | $canBeCall^ |
197 | Foo::$canBeCall^ |
198 | } |
199 | }; |
200 | |
201 | struct Derived : Foo { |
202 | Derived() { |
203 | Foo::$canBeCall^ |
204 | } |
205 | }; |
206 | |
207 | struct OtherClass { |
208 | OtherClass() { |
209 | Foo f; |
210 | f.$canBeCall^ |
211 | &Foo::$cannotBeCall^ |
212 | } |
213 | }; |
214 | |
215 | int main() { |
216 | Foo f; |
217 | f.$canBeCall^ |
218 | &Foo::$cannotBeCall^ |
219 | } |
220 | )cpp" ); |
221 | |
222 | for (const auto &P : Code.points("canBeCall" )) { |
223 | auto Results = CollectCompletedFunctions(Code.code(), P); |
224 | EXPECT_THAT(Results, Contains(AllOf(named("method" ), isStatic(false), |
225 | canBeCall(true)))); |
226 | } |
227 | |
228 | for (const auto &P : Code.points("cannotBeCall" )) { |
229 | auto Results = CollectCompletedFunctions(Code.code(), P); |
230 | EXPECT_THAT(Results, Contains(AllOf(named("method" ), isStatic(false), |
231 | canBeCall(false)))); |
232 | } |
233 | |
234 | // static method can always be a call |
235 | for (const auto &P : Code.points()) { |
236 | auto Results = CollectCompletedFunctions(Code.code(), P); |
237 | EXPECT_THAT(Results, Contains(AllOf(named("staticMethod" ), isStatic(true), |
238 | canBeCall(true)))); |
239 | } |
240 | } |
241 | |
242 | TEST(SemaCodeCompleteTest, VisitedNSForValidQualifiedId) { |
243 | auto VisitedNS = runCodeCompleteOnCode(R"cpp( |
244 | namespace ns1 {} |
245 | namespace ns2 {} |
246 | namespace ns3 {} |
247 | namespace ns3 { namespace nns3 {} } |
248 | |
249 | namespace foo { |
250 | using namespace ns1; |
251 | namespace ns4 {} // not visited |
252 | namespace { using namespace ns2; } |
253 | inline namespace bar { using namespace ns3::nns3; } |
254 | } // foo |
255 | namespace ns { foo::^ } |
256 | )cpp" ) |
257 | .VisitedNamespaces; |
258 | EXPECT_THAT(VisitedNS, UnorderedElementsAre("foo" , "ns1" , "ns2" , "ns3::nns3" , |
259 | "foo::(anonymous)" )); |
260 | } |
261 | |
262 | TEST(SemaCodeCompleteTest, VisitedNSForInvalidQualifiedId) { |
263 | auto VisitedNS = runCodeCompleteOnCode(R"cpp( |
264 | namespace na {} |
265 | namespace ns1 { |
266 | using namespace na; |
267 | foo::^ |
268 | } |
269 | )cpp" ) |
270 | .VisitedNamespaces; |
271 | EXPECT_THAT(VisitedNS, UnorderedElementsAre("ns1" , "na" )); |
272 | } |
273 | |
274 | TEST(SemaCodeCompleteTest, VisitedNSWithoutQualifier) { |
275 | auto VisitedNS = runCodeCompleteOnCode(R"cpp( |
276 | namespace n1 { |
277 | namespace n2 { |
278 | void f(^) {} |
279 | } |
280 | } |
281 | )cpp" ) |
282 | .VisitedNamespaces; |
283 | EXPECT_THAT(VisitedNS, UnorderedElementsAre("n1" , "n1::n2" )); |
284 | } |
285 | |
286 | TEST(PreferredTypeTest, BinaryExpr) { |
287 | // Check various operations for arithmetic types. |
288 | StringRef Code = R"cpp( |
289 | void test(int x) { |
290 | x = ^10; |
291 | x += ^10; x -= ^10; x *= ^10; x /= ^10; x %= ^10; |
292 | x + ^10; x - ^10; x * ^10; x / ^10; x % ^10; |
293 | })cpp" ; |
294 | EXPECT_THAT(collectPreferredTypes(Code), Each("int" )); |
295 | |
296 | Code = R"cpp( |
297 | void test(float x) { |
298 | x = ^10; |
299 | x += ^10; x -= ^10; x *= ^10; x /= ^10; x %= ^10; |
300 | x + ^10; x - ^10; x * ^10; x / ^10; x % ^10; |
301 | })cpp" ; |
302 | EXPECT_THAT(collectPreferredTypes(Code), Each("float" )); |
303 | |
304 | // Pointer types. |
305 | Code = R"cpp( |
306 | void test(int *ptr) { |
307 | ptr - ^ptr; |
308 | ptr = ^ptr; |
309 | })cpp" ; |
310 | EXPECT_THAT(collectPreferredTypes(Code), Each("int *" )); |
311 | |
312 | Code = R"cpp( |
313 | void test(int *ptr) { |
314 | ptr + ^10; |
315 | ptr += ^10; |
316 | ptr -= ^10; |
317 | })cpp" ; |
318 | { |
319 | std::string PtrDiff; |
320 | auto Types = collectPreferredTypes(Code, &PtrDiff); |
321 | EXPECT_THAT(Types, Each(PtrDiff)); |
322 | } |
323 | |
324 | // Comparison operators. |
325 | Code = R"cpp( |
326 | void test(int i) { |
327 | i <= ^1; i < ^1; i >= ^1; i > ^1; i == ^1; i != ^1; |
328 | } |
329 | )cpp" ; |
330 | EXPECT_THAT(collectPreferredTypes(Code), Each("int" )); |
331 | |
332 | Code = R"cpp( |
333 | void test(int *ptr) { |
334 | ptr <= ^ptr; ptr < ^ptr; ptr >= ^ptr; ptr > ^ptr; |
335 | ptr == ^ptr; ptr != ^ptr; |
336 | } |
337 | )cpp" ; |
338 | EXPECT_THAT(collectPreferredTypes(Code), Each("int *" )); |
339 | |
340 | // Relational operations. |
341 | Code = R"cpp( |
342 | void test(int i, int *ptr) { |
343 | i && ^1; i || ^1; |
344 | ptr && ^1; ptr || ^1; |
345 | } |
346 | )cpp" ; |
347 | EXPECT_THAT(collectPreferredTypes(Code), Each("_Bool" )); |
348 | |
349 | // Bitwise operations. |
350 | Code = R"cpp( |
351 | void test(long long ll) { |
352 | ll | ^1; ll & ^1; |
353 | } |
354 | )cpp" ; |
355 | EXPECT_THAT(collectPreferredTypes(Code), Each("long long" )); |
356 | |
357 | Code = R"cpp( |
358 | enum A {}; |
359 | void test(A a) { |
360 | a | ^1; a & ^1; |
361 | } |
362 | )cpp" ; |
363 | EXPECT_THAT(collectPreferredTypes(Code), Each("A" )); |
364 | |
365 | Code = R"cpp( |
366 | enum class A {}; |
367 | void test(A a) { |
368 | // This is technically illegal with the 'enum class' without overloaded |
369 | // operators, but we pretend it's fine. |
370 | a | ^a; a & ^a; |
371 | } |
372 | )cpp" ; |
373 | EXPECT_THAT(collectPreferredTypes(Code), Each("A" )); |
374 | |
375 | // Binary shifts. |
376 | Code = R"cpp( |
377 | void test(int i, long long ll) { |
378 | i << ^1; ll << ^1; |
379 | i <<= ^1; i <<= ^1; |
380 | i >> ^1; ll >> ^1; |
381 | i >>= ^1; i >>= ^1; |
382 | } |
383 | )cpp" ; |
384 | EXPECT_THAT(collectPreferredTypes(Code), Each("int" )); |
385 | |
386 | // Comma does not provide any useful information. |
387 | Code = R"cpp( |
388 | class Cls {}; |
389 | void test(int i, int* ptr, Cls x) { |
390 | (i, ^i); |
391 | (ptr, ^ptr); |
392 | (x, ^x); |
393 | } |
394 | )cpp" ; |
395 | EXPECT_THAT(collectPreferredTypes(Code), Each("NULL TYPE" )); |
396 | |
397 | // User-defined types do not take operator overloading into account. |
398 | // However, they provide heuristics for some common cases. |
399 | Code = R"cpp( |
400 | class Cls {}; |
401 | void test(Cls c) { |
402 | // we assume arithmetic and comparions ops take the same type. |
403 | c + ^c; c - ^c; c * ^c; c / ^c; c % ^c; |
404 | c == ^c; c != ^c; c < ^c; c <= ^c; c > ^c; c >= ^c; |
405 | // same for the assignments. |
406 | c = ^c; c += ^c; c -= ^c; c *= ^c; c /= ^c; c %= ^c; |
407 | } |
408 | )cpp" ; |
409 | EXPECT_THAT(collectPreferredTypes(Code), Each("Cls" )); |
410 | |
411 | Code = R"cpp( |
412 | class Cls {}; |
413 | void test(Cls c) { |
414 | // we assume relational ops operate on bools. |
415 | c && ^c; c || ^c; |
416 | } |
417 | )cpp" ; |
418 | EXPECT_THAT(collectPreferredTypes(Code), Each("_Bool" )); |
419 | |
420 | Code = R"cpp( |
421 | class Cls {}; |
422 | void test(Cls c) { |
423 | // we make no assumptions about the following operators, since they are |
424 | // often overloaded with a non-standard meaning. |
425 | c << ^c; c >> ^c; c | ^c; c & ^c; |
426 | c <<= ^c; c >>= ^c; c |= ^c; c &= ^c; |
427 | } |
428 | )cpp" ; |
429 | EXPECT_THAT(collectPreferredTypes(Code), Each("NULL TYPE" )); |
430 | } |
431 | |
432 | TEST(PreferredTypeTest, Members) { |
433 | StringRef Code = R"cpp( |
434 | struct vector { |
435 | int *begin(); |
436 | vector clone(); |
437 | }; |
438 | |
439 | void test(int *a) { |
440 | a = ^vector().^clone().^begin(); |
441 | } |
442 | )cpp" ; |
443 | EXPECT_THAT(collectPreferredTypes(Code), Each("int *" )); |
444 | } |
445 | |
446 | TEST(PreferredTypeTest, Conditions) { |
447 | StringRef Code = R"cpp( |
448 | struct vector { |
449 | bool empty(); |
450 | }; |
451 | |
452 | void test() { |
453 | if (^vector().^empty()) {} |
454 | while (^vector().^empty()) {} |
455 | for (; ^vector().^empty();) {} |
456 | } |
457 | )cpp" ; |
458 | EXPECT_THAT(collectPreferredTypes(Code), Each("_Bool" )); |
459 | } |
460 | |
461 | TEST(PreferredTypeTest, InitAndAssignment) { |
462 | StringRef Code = R"cpp( |
463 | struct vector { |
464 | int* begin(); |
465 | }; |
466 | |
467 | void test() { |
468 | const int* x = ^vector().^begin(); |
469 | x = ^vector().^begin(); |
470 | |
471 | if (const int* y = ^vector().^begin()) {} |
472 | } |
473 | )cpp" ; |
474 | EXPECT_THAT(collectPreferredTypes(Code), Each("const int *" )); |
475 | } |
476 | |
477 | TEST(PreferredTypeTest, UnaryExprs) { |
478 | StringRef Code = R"cpp( |
479 | void test(long long a) { |
480 | a = +^a; |
481 | a = -^a |
482 | a = ++^a; |
483 | a = --^a; |
484 | } |
485 | )cpp" ; |
486 | EXPECT_THAT(collectPreferredTypes(Code), Each("long long" )); |
487 | |
488 | Code = R"cpp( |
489 | void test(int a, int *ptr) { |
490 | !^a; |
491 | !^ptr; |
492 | !!!^a; |
493 | |
494 | a = !^a; |
495 | a = !^ptr; |
496 | a = !!!^a; |
497 | } |
498 | )cpp" ; |
499 | EXPECT_THAT(collectPreferredTypes(Code), Each("_Bool" )); |
500 | |
501 | Code = R"cpp( |
502 | void test(int a) { |
503 | const int* x = &^a; |
504 | } |
505 | )cpp" ; |
506 | EXPECT_THAT(collectPreferredTypes(Code), Each("const int" )); |
507 | |
508 | Code = R"cpp( |
509 | void test(int *a) { |
510 | int x = *^a; |
511 | int &r = *^a; |
512 | } |
513 | )cpp" ; |
514 | EXPECT_THAT(collectPreferredTypes(Code), Each("int *" )); |
515 | |
516 | Code = R"cpp( |
517 | void test(int a) { |
518 | *^a; |
519 | &^a; |
520 | } |
521 | |
522 | )cpp" ; |
523 | } |
524 | |
525 | TEST(PreferredTypeTest, ParenExpr) { |
526 | StringRef Code = R"cpp( |
527 | const int *i = ^(^(^(^10))); |
528 | )cpp" ; |
529 | EXPECT_THAT(collectPreferredTypes(Code), Each("const int *" )); |
530 | } |
531 | |
532 | TEST(PreferredTypeTest, FunctionArguments) { |
533 | StringRef Code = R"cpp( |
534 | void foo(const int*); |
535 | |
536 | void bar(const int*); |
537 | void bar(const int*, int b); |
538 | |
539 | struct vector { |
540 | const int *data(); |
541 | }; |
542 | void test() { |
543 | foo(^(^(^(^vec^tor^().^da^ta^())))); |
544 | bar(^(^(^(^vec^tor^().^da^ta^())))); |
545 | } |
546 | )cpp" ; |
547 | EXPECT_THAT(collectPreferredTypes(Code), Each("const int *" )); |
548 | |
549 | Code = R"cpp( |
550 | void bar(int, volatile double *); |
551 | void bar(int, volatile double *, int, int); |
552 | |
553 | struct vector { |
554 | double *data(); |
555 | }; |
556 | |
557 | struct class_members { |
558 | void bar(int, volatile double *); |
559 | void bar(int, volatile double *, int, int); |
560 | }; |
561 | void test() { |
562 | bar(10, ^(^(^(^vec^tor^().^da^ta^())))); |
563 | class_members().bar(10, ^(^(^(^vec^tor^().^da^ta^())))); |
564 | } |
565 | )cpp" ; |
566 | EXPECT_THAT(collectPreferredTypes(Code), Each("volatile double *" )); |
567 | |
568 | Code = R"cpp( |
569 | namespace ns { |
570 | struct vector { |
571 | }; |
572 | } |
573 | void accepts_vector(ns::vector); |
574 | |
575 | void test() { |
576 | accepts_vector(^::^ns::^vector()); |
577 | } |
578 | )cpp" ; |
579 | EXPECT_THAT(collectPreferredTypes(Code), Each("ns::vector" )); |
580 | |
581 | Code = R"cpp( |
582 | template <class T> |
583 | struct vector { using self = vector; }; |
584 | |
585 | void accepts_vector(vector<int>); |
586 | int foo(int); |
587 | |
588 | void test() { |
589 | accepts_vector(^::^vector<decltype(foo(1))>::^self); |
590 | } |
591 | )cpp" ; |
592 | EXPECT_THAT(collectPreferredTypes(Code), Each("vector<int>" )); |
593 | } |
594 | |
595 | TEST(PreferredTypeTest, NoCrashOnInvalidTypes) { |
596 | StringRef Code = R"cpp( |
597 | auto x = decltype(&1)(^); |
598 | auto y = new decltype(&1)(^); |
599 | // GNU decimal type extension is not supported in clang. |
600 | auto z = new _Decimal128(^); |
601 | void foo() { (void)(foo)(^); } |
602 | )cpp" ; |
603 | EXPECT_THAT(collectPreferredTypes(Code), Each("NULL TYPE" )); |
604 | } |
605 | |
606 | } // namespace |
607 | |