cpp_ls 0.3.0

A C, C++ and CUDA language server.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
//! **The card a reported hover needed** — a member of a class-template instantiation.

use std::path::PathBuf;

use cpp_code_analysis::file::coordinates::{FileOffset, ReadingOffset};
use cpp_code_analysis::{DiskFiles, OpenDocuments, Session, SessionFiles, WatchFilter};

/// `Pair<int, int>{1, 2}.sum()` answered "`int` has no member `sum`".
///
/// Two defects met here, and both are fixed: the initialisation's type was the whole spelling as a *name*
/// (`Named { name: "Pair<int, int>" }`, no arguments) rather than the instantiation, so no substitution reached `sum`'s
/// written `T`; and the card read the object's type at its **last byte**, which for a braced initialisation is inside the
/// list — `int`, the first element's type.
///
/// The analysis-layer half is pinned by `cpp_code_analysis`'s
/// `a_braced_initialisation_of_a_class_template_keeps_its_arguments`; this is the card.
#[test]
fn a_member_of_a_class_template_instantiation_is_named() {
    let root: PathBuf = std::env::temp_dir().join("cppls-hover-template");
    let _ = std::fs::remove_dir_all(&root);
    std::fs::create_dir_all(&root).expect("a project directory");
    let file = root.join("a.cpp");
    let source = "template <typename T, typename U = int>\n\
                  struct Pair {\n\
                      T first;\n\
                      U second;\n\
                      T sum() const { return first + second; }\n\
                  };\n\
                  void f() {\n\
                      int a = Pair<int, int>{1, 2}.sum();\n\
                  }\n";
    std::fs::write(&file, source).expect("the fixture writes");

    let mut session = Session::open(
        &root,
        SessionFiles::new(OpenDocuments::new(), DiskFiles),
        WatchFilter::new(&root),
    );
    session.add_project_files([file.clone()]);
    session.did_open(&file, source);
    session.catch_up(&file);
    let _ = session.cook(&file);
    let _ = session.index_everything();

    let view = session.view_of_the_file(&file).expect("the written text");
    let at =
        source.find("Pair<int, int>{1, 2}.sum").expect("the use") + "Pair<int, int>{1, 2}.".len();
    let hover = cpp_ls::handlers::hover::hover(
        &session,
        &view,
        ReadingOffset::new(at),
        FileOffset::new(at),
    )
    .expect("a member has a card");
    let text = match hover.contents {
        lsp_types::HoverContents::Markup(markup) => markup.value,
        other => format!("{other:?}"),
    };
    assert!(
        !text.contains("has no member"),
        "the member is resolved through the instantiation: {text}"
    );
    assert!(text.contains("sum"), "the card names the member: {text}");

    let _ = std::fs::remove_dir_all(&root);
}

/// **A member's card speaks in the instance's terms** — `Box<int>::query` returns `std::optional<int>`.
///
/// Reported with a screenshot: hovering `box.query(1)` on a `Box<int>` printed `std::optional<V> query(int key)` — the
/// class's own spelling. The member list the card was built from had already substituted the arguments
/// (`members_of("Box<int>")` answers `returns: Some("std::optional<int>")`, measured); what lost them was the card's
/// own re-ask of the index, whose condition was `nothing to render || no annotation` — and every ordinary declaration
/// has no annotation, so the substituted fact was replaced by the index's unsubstituted copy.
#[test]
fn a_member_of_an_instance_answers_in_the_instance_terms() {
    let root: PathBuf = std::env::temp_dir().join("cppls-hover-instance");
    let _ = std::fs::remove_dir_all(&root);
    std::fs::create_dir_all(&root).expect("a project directory");
    let file = root.join("a.cpp");
    let source = "template <class V>\n\
                  struct Box {\n\
                      std::optional<V> query(int key);\n\
                  };\n\
                  struct C {\n\
                      Box<int> box;\n\
                      void f() {\n\
                          auto x = box.query(1);\n\
                      }\n\
                  };\n";
    std::fs::write(&file, source).expect("the fixture writes");

    let mut session = Session::open(
        &root,
        SessionFiles::new(OpenDocuments::new(), DiskFiles),
        WatchFilter::new(&root),
    );
    session.add_project_files([file.clone()]);
    session.did_open(&file, source);
    session.catch_up(&file);
    let _ = session.cook(&file);
    let _ = session.index_everything();

    let view = session.view_of_the_file(&file).expect("the written text");
    let at = source.find("box.query").expect("the use") + "box.".len();
    let hover = cpp_ls::handlers::hover::hover(
        &session,
        &view,
        ReadingOffset::new(at),
        FileOffset::new(at),
    )
    .expect("a member has a card");
    let text = match hover.contents {
        lsp_types::HoverContents::Markup(markup) => markup.value,
        other => format!("{other:?}"),
    };
    assert!(
        text.contains("std::optional<int>"),
        "the arguments are substituted into the member's return type: {text}"
    );
    assert!(
        !text.contains("std::optional<V>"),
        "and the class's own parameter is not what the reader sees: {text}"
    );

    let _ = std::fs::remove_dir_all(&root);
}

/// `cfoo.call(...)` on a `const Foo` showed the **non-const** `call`, and `Bar`'s `requires`-clause never appeared.
///
/// A class may declare a name twice and tell the two apart by `const` (and by a trailing `requires`). The object's
/// constness is not in the type model yet — `type_of` is `Foo` for both `Foo foo;` and `const Foo cfoo;` — so the card
/// lists **every** declaration of the name, with the qualifiers the facts now record; the reader's own line says which
/// object they are calling it on.
#[test]
fn a_member_declared_twice_shows_both_declarations_and_their_clauses() {
    let root: PathBuf = std::env::temp_dir().join("cppls-hover-qualifiers");
    let _ = std::fs::remove_dir_all(&root);
    std::fs::create_dir_all(&root).expect("a project directory");
    let file = root.join("a.cpp");
    let source = "struct Foo {\n\
                  template <typename Fn> void call(Fn &&fn) {}\n\
                  template <typename Fn> void call(Fn &&fn) const {}\n\
                  };\n\
                  struct Bar {\n\
                  template <typename Fn> void call(Fn &&fn) requires requires { fn(*this); } {}\n\
                  template <typename Fn> void call(Fn &&fn) const requires requires { fn(*this); } {}\n\
                  };\n\
                  void f() {\n\
                  const Foo cfoo;\n\
                  cfoo.call([](auto &&x) {});\n\
                  const Bar cbar;\n\
                  cbar.call([](auto &&x) {});\n\
                  }\n";
    std::fs::write(&file, source).expect("the fixture writes");

    let mut session = Session::open(
        &root,
        SessionFiles::new(OpenDocuments::new(), DiskFiles),
        WatchFilter::new(&root),
    );
    session.add_project_files([file.clone()]);
    session.did_open(&file, source);
    session.catch_up(&file);
    let _ = session.cook(&file);
    let _ = session.index_everything();
    let view = session.view_of_the_file(&file).expect("the written text");

    for (needle, must_say) in [
        ("cfoo.call", "void call(Fn&& fn) const"),
        ("cbar.call", "const requires requires"),
    ] {
        let line = source.find(needle).expect("the use");
        let at = line + needle.find("call").expect("the member name");
        let hover = cpp_ls::handlers::hover::hover(
            &session,
            &view,
            ReadingOffset::new(at + 1),
            FileOffset::new(at + 1),
        )
        .expect("a member has a card");
        let text = match hover.contents {
            lsp_types::HoverContents::Markup(markup) => markup.value,
            other => format!("{other:?}"),
        };
        assert!(
            text.contains(must_say),
            "the card must say {must_say:?}: {text}"
        );
        assert!(
            text.contains("const Foo")
                || text.contains("const Bar")
                || text.contains("`const` object"),
            "and it must say which one a const object may call: {text}"
        );
    }

    let _ = std::fs::remove_dir_all(&root);
}

/// **A local whose type is deduced shows the type it was deduced to** — reported with a screenshot: hovering
/// `auto &syntaxTree = opSyntaxTree.value();` showed "`syntaxTree` — a variable, declared in a function body" and
/// **no type at all**, while the inlay hint drawn beside the same name said `LuaSyntaxTree&`.
///
/// The fact records what the file *wrote* (`auto&`), which is the spelling a lookup wants and no answer at all to
/// "what is this". The engine has the answer — `type_at`, the same question the inlay asks — and the card now shows it.
#[test]
fn a_deduced_local_shows_its_type() {
    let root: PathBuf = std::env::temp_dir().join("cppls-hover-deduced");
    let _ = std::fs::remove_dir_all(&root);
    std::fs::create_dir_all(&root).expect("a project directory");
    let file = root.join("a.cpp");
    let source = "struct LuaSyntaxTree {\n\
                      bool HasError() const { return false; }\n\
                  };\n\
                  struct Holder {\n\
                      LuaSyntaxTree& value();\n\
                  };\n\
                  void f() {\n\
                      Holder op;\n\
                      auto &tree = op.value();\n\
                      bool bad = tree.HasError();\n\
                  }\n";
    std::fs::write(&file, source).expect("the fixture writes");

    let mut session = Session::open(
        &root,
        SessionFiles::new(OpenDocuments::new(), DiskFiles),
        WatchFilter::new(&root),
    );
    session.add_project_files([file.clone()]);
    session.did_open(&file, source);
    session.catch_up(&file);
    let _ = session.cook(&file);
    let _ = session.index_everything();

    let view = session.view_of_the_file(&file).expect("the written text");
    let at = source.find("tree.HasError").expect("the use");
    let hover = cpp_ls::handlers::hover::hover(
        &session,
        &view,
        ReadingOffset::new(at),
        FileOffset::new(at),
    )
    .expect("a local has a card");
    let text = match hover.contents {
        lsp_types::HoverContents::Markup(markup) => markup.value,
        other => format!("{other:?}"),
    };
    assert!(
        text.contains("LuaSyntaxTree&"),
        "the card shows the deduced type: {text}"
    );

    let _ = std::fs::remove_dir_all(&root);
}

/// **A smart pointer's arrow reaches its pointee's member in the card too** — "shared_ptr 没有 -> 补全和 hover 识别".
///
/// Same rule as the completion (`element_type` is the pointer protocol; libstdc++ keeps `operator->` two bases down, in
/// `__shared_ptr_access`), asked through the card: before the fix this said "`std::shared_ptr<State>` has no member
/// `called`". The base here is declared and nothing more, which is the state the real project was in.
#[test]
fn a_member_through_a_smart_pointers_arrow_is_named() {
    let root: PathBuf = std::env::temp_dir().join("cppls-hover-arrow");
    let _ = std::fs::remove_dir_all(&root);
    std::fs::create_dir_all(&root).expect("a project directory");
    let file = root.join("a.cpp");
    let source = "namespace std {\n\
                      template <typename T, int Lp>\n\
                      class __shared_ptr;\n\
                      template <typename T>\n\
                      class shared_ptr : public __shared_ptr<T, 2> {\n\
                      public:\n\
                          using element_type = T;\n\
                          shared_ptr() = default;\n\
                      };\n\
                  }\n\
                  struct State {\n\
                      bool called;\n\
                  };\n\
                  std::shared_ptr<State> state_;\n\
                  void f() {\n\
                      state_->called = true;\n\
                  }\n";
    std::fs::write(&file, source).expect("the fixture writes");

    let mut session = Session::open(
        &root,
        SessionFiles::new(OpenDocuments::new(), DiskFiles),
        WatchFilter::new(&root),
    );
    session.add_project_files([file.clone()]);
    session.did_open(&file, source);
    session.catch_up(&file);
    let _ = session.cook(&file);
    let _ = session.index_everything();

    let view = session.view_of_the_file(&file).expect("the written text");
    let at = source.find("called = true").expect("the member");
    let hover = cpp_ls::handlers::hover::hover(
        &session,
        &view,
        ReadingOffset::new(at),
        FileOffset::new(at),
    )
    .expect("a member has a card");
    let text = match hover.contents {
        lsp_types::HoverContents::Markup(markup) => markup.value,
        other => format!("{other:?}"),
    };
    assert!(
        !text.contains("has no member"),
        "the member is reached through the handle: {text}"
    );
    assert!(
        text.contains("State") && text.contains("called"),
        "the card says the member belongs to the pointee: {text}"
    );
    assert!(text.contains("called"), "the card names the member: {text}");

    let _ = std::fs::remove_dir_all(&root);
}

/// **A local variable is never answered by a same-named declaration from the index.**
///
/// Reported from `EmmyLuaCodeStyle`: hovering the variable in
/// `auto result = std::make_shared<lsp::DocumentFormattingResult>();` showed
/// `` `codecvt_base::result` — a type of type `int`, in `codecvt_base`, declared in `xlocale:581:11` `` — a real
/// `using result = int;` from the standard library — and hovering the variable `object` in `LSP.cpp:12` showed a
/// static member function `object`.
///
/// The cause was in the **card**, not in the lookup: `declaration_markdown` re-asks the index for a declaration whose
/// fact has no type, no return type and no parameter list — which is every fact built from a **binding** — and a
/// local's `qualified_name()` is its bare spelling, because a function body contributes no scope segment. So the bare
/// name found another entity entirely. `definitions`, `definition` and `type_at` had the local right all along.
/// See `docs/buglist.md` section 7.
#[test]
fn a_local_is_not_answered_by_a_same_named_declaration_from_the_index() {
    let root: PathBuf = std::env::temp_dir().join("cppls-hover-local");
    let _ = std::fs::remove_dir_all(&root);
    std::fs::create_dir_all(&root).expect("a project directory");
    let header = root.join("standard.h");
    std::fs::write(
        &header,
        "namespace std {\nstruct codecvt_base {\n    using result = int;\n};\n}\n",
    )
    .expect("the header writes");
    let file = root.join("a.cpp");
    let source = "#include \"standard.h\"\nvoid f() {\n    auto result = 1;\n}\n";
    std::fs::write(&file, source).expect("the fixture writes");

    let mut session = Session::open(
        &root,
        SessionFiles::new(OpenDocuments::new(), DiskFiles),
        WatchFilter::new(&root),
    );
    session.add_project_files([file.clone(), header.clone()]);
    session.did_open(&file, source);
    session.catch_up(&file);
    let _ = session.cook(&file);
    let _ = session.index_everything();

    let view = session.view_of_the_file(&file).expect("the written text");
    let at = source.find("result").expect("the local") + 1;
    let text = cpp_ls::handlers::hover::hover_text(
        &session,
        &view,
        ReadingOffset::new(at),
        FileOffset::new(at),
    )
    .expect("the local has a card");

    assert!(
        !text.contains("codecvt_base"),
        "the card is about the local, not about the standard library's `result`: {text}"
    );
    assert!(
        text.contains("a variable"),
        "and it says what the local is: {text}"
    );
    assert!(text.contains("int"), "and what it deduced to: {text}");

    let _ = std::fs::remove_dir_all(&root);
}

/// **A documentation link's target has a card** — Rust's ``[`Widget::grow`]``, hovered.
///
/// The link is read from the file's own text (a comment is trivia, so the rendering has none of it) and the target
/// is resolved with the same query a jump uses, so the card and the jump cannot disagree.
#[test]
fn a_documentation_links_target_has_a_card() {
    let root: PathBuf = std::env::temp_dir().join("cppls-hover-doc-link");
    let _ = std::fs::remove_dir_all(&root);
    std::fs::create_dir_all(&root).expect("a project directory");
    let file = root.join("a.cpp");
    let source = "\
struct Widget {
    /// See [`Widget::grow`] for the rule.
    void grow(int by);
};
";
    std::fs::write(&file, source).expect("the fixture writes");

    let mut session = Session::open(
        &root,
        SessionFiles::new(OpenDocuments::new(), DiskFiles),
        WatchFilter::new(&root),
    );
    session.add_project_files([file.clone()]);
    session.did_open(&file, source);
    session.catch_up(&file);
    let _ = session.cook(&file);
    let _ = session.index_everything();

    let view = session.view_of_the_file(&file).expect("the written text");
    let at = source.find("See [`").expect("the fixture") + "See [`Widget::".len();
    let hover = cpp_ls::handlers::hover::hover(
        &session,
        &view,
        ReadingOffset::new(at),
        FileOffset::new(at),
    )
    .expect("a link has a card");
    let text = match hover.contents {
        lsp_types::HoverContents::Markup(markup) => markup.value,
        other => format!("{other:?}"),
    };
    assert!(
        text.contains("void grow(int by)"),
        "the card is the declaration the link names: {text}"
    );

    let _ = std::fs::remove_dir_all(&root);
}